maintenance_on_steroids 0.2.0
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/CHANGELOG.md +267 -0
- data/LICENSE.txt +21 -0
- data/README.md +948 -0
- data/app/controllers/maintenance_on_steroids/application_controller.rb +66 -0
- data/app/controllers/maintenance_on_steroids/dashboard_controller.rb +18 -0
- data/app/controllers/maintenance_on_steroids/jobs_controller.rb +59 -0
- data/app/controllers/maintenance_on_steroids/runs_controller.rb +223 -0
- data/app/jobs/maintenance_on_steroids/run_job.rb +292 -0
- data/app/models/maintenance_on_steroids/application_record.rb +6 -0
- data/app/models/maintenance_on_steroids/artifact.rb +255 -0
- data/app/models/maintenance_on_steroids/run.rb +310 -0
- data/app/views/layouts/maintenance_on_steroids/application.html.erb +48 -0
- data/app/views/maintenance_on_steroids/dashboard/index.html.erb +163 -0
- data/app/views/maintenance_on_steroids/jobs/index.html.erb +35 -0
- data/app/views/maintenance_on_steroids/jobs/show.html.erb +107 -0
- data/app/views/maintenance_on_steroids/jobs/source.html.erb +79 -0
- data/app/views/maintenance_on_steroids/runs/new.html.erb +98 -0
- data/app/views/maintenance_on_steroids/runs/show.html.erb +410 -0
- data/app/views/maintenance_on_steroids/shared/_auto_refresh.html.erb +85 -0
- data/app/views/maintenance_on_steroids/shared/_javascript.html.erb +16 -0
- data/app/views/maintenance_on_steroids/shared/_pager.html.erb +20 -0
- data/app/views/maintenance_on_steroids/shared/_styles.html.erb +649 -0
- data/app/views/maintenance_on_steroids/shared/_task_list_item.html.erb +31 -0
- data/config/routes.rb +22 -0
- data/lib/generators/maintenance_on_steroids/install/install_generator.rb +56 -0
- data/lib/generators/maintenance_on_steroids/install/templates/create_maintenance_on_steroids_tables.rb.erb +56 -0
- data/lib/generators/maintenance_on_steroids/install/templates/initializer.rb +42 -0
- data/lib/generators/maintenance_on_steroids/job/job_generator.rb +17 -0
- data/lib/generators/maintenance_on_steroids/job/templates/job.rb.erb +30 -0
- data/lib/maintenance_on_steroids/about_dsl.rb +51 -0
- data/lib/maintenance_on_steroids/artifact_dsl.rb +82 -0
- data/lib/maintenance_on_steroids/artifacts_proxy.rb +205 -0
- data/lib/maintenance_on_steroids/callbacks_dsl.rb +61 -0
- data/lib/maintenance_on_steroids/csv_artifact.rb +88 -0
- data/lib/maintenance_on_steroids/engine.rb +26 -0
- data/lib/maintenance_on_steroids/form_dsl.rb +63 -0
- data/lib/maintenance_on_steroids/instrumentation.rb +33 -0
- data/lib/maintenance_on_steroids/job_dsl.rb +61 -0
- data/lib/maintenance_on_steroids/job_registry.rb +83 -0
- data/lib/maintenance_on_steroids/jsonb_artifact.rb +39 -0
- data/lib/maintenance_on_steroids/params_proxy.rb +93 -0
- data/lib/maintenance_on_steroids/task.rb +109 -0
- data/lib/maintenance_on_steroids/text_artifact.rb +74 -0
- data/lib/maintenance_on_steroids/version.rb +3 -0
- data/lib/maintenance_on_steroids.rb +187 -0
- metadata +125 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 0a7be3f77841e342ff75211412864af8f0728ac542ebe1909104f8eb4876c1a6
|
|
4
|
+
data.tar.gz: 64de5cb0d0278e33b2548b04c3b11b2a1491eaabcf0eee2e4d180e5f74661f5a
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 54ee12177a99ae2cf304c1fd0310d1577eb48ffaff9afcc6e1dbc4157d242b5a940d7ced4ba9f693d27adee7ea79421b261e0b91f95ab2de334e74e89a7bebf2
|
|
7
|
+
data.tar.gz: b0d39045b36f8ac1819f27af6e1cdc8a5b7e4a486aab60e991362e279e71cf3677dd35e1a75b8585f791159df4b191893d6895c1128646e577451b0aedbf3560
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
### Security
|
|
11
|
+
|
|
12
|
+
- Require Rails 8.1.3.1 or newer within Rails 8, and add dependency auditing and
|
|
13
|
+
Brakeman to CI. Audit the host application's bundle as well.
|
|
14
|
+
- Resolve task names only against loaded task subclasses, including in workers.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Claim runs atomically and fence engine writes with a per-execution token.
|
|
19
|
+
Duplicate deliveries, paused runs and obsolete job IDs cannot restart work.
|
|
20
|
+
Stale workers cannot overwrite a resumed run's progress or artifacts.
|
|
21
|
+
- Detect refused queue submissions and Continuable retries. Prepare resume
|
|
22
|
+
metadata before dispatch so fast workers retain their new results and errors.
|
|
23
|
+
- Commit collection accumulator output and its cursor together. Flush final
|
|
24
|
+
output before committing completion; persistence failures now error the run.
|
|
25
|
+
- Refresh callable heartbeats at checkpoints. Add optional `enqueued_threshold:`
|
|
26
|
+
to stale-run recovery for abandoned dispatches.
|
|
27
|
+
- Check text/CSV append sizes before growing their buffers. Clarify at-least-once
|
|
28
|
+
side effects, callable restart behavior and artifact memory limits.
|
|
29
|
+
|
|
30
|
+
- Completion, pause and cancel callbacks produce output before the final status
|
|
31
|
+
is committed. Flush failures raise from `ArtifactsProxy#flush!` after attempting
|
|
32
|
+
the remaining artifacts. Keep artifacts small: collection checkpoints now
|
|
33
|
+
persist dirty output after every record.
|
|
34
|
+
|
|
35
|
+
## [0.2.0] - 2026-08-24
|
|
36
|
+
|
|
37
|
+
First version published to RubyGems. The 0.1.1 and 0.1.0 entries below are
|
|
38
|
+
development milestones -- neither was tagged or released, so no one ever ran
|
|
39
|
+
them. The minor bump reflects the four public APIs added since 0.1.1.
|
|
40
|
+
|
|
41
|
+
### Security
|
|
42
|
+
|
|
43
|
+
- **A blank HTTP Basic password no longer counts as configured.** The check
|
|
44
|
+
added in 0.1.1 compared only against the shipped `"secret"`, so a nil
|
|
45
|
+
password -- the result of a missing or misspelled credentials key, which is
|
|
46
|
+
exactly the pattern the generated initializer recommends -- passed as
|
|
47
|
+
configured, booted production without a warning, and let `admin` plus an
|
|
48
|
+
empty password through, because `secure_compare(supplied, "")` matches. Blank
|
|
49
|
+
is now treated as unconfigured, and the controller refuses to authenticate at
|
|
50
|
+
all while the configured password is blank.
|
|
51
|
+
|
|
52
|
+
### Added
|
|
53
|
+
|
|
54
|
+
- **`Run.prune!(older_than:)`** deletes finished runs and their artifacts.
|
|
55
|
+
Nothing expired these rows before, so a long-lived app accumulated every run,
|
|
56
|
+
backtrace and stored blob indefinitely. Active and paused runs are never
|
|
57
|
+
pruned regardless of age.
|
|
58
|
+
- **`MaintenanceOnSteroids.max_artifact_size`** (default 64 MB) caps a single
|
|
59
|
+
stored artifact. Artifacts are buffered in worker memory and stored in one
|
|
60
|
+
row, so an unbounded export previously OOM-killed the worker; it now fails
|
|
61
|
+
the run with a message naming the artifact and its size.
|
|
62
|
+
- **`Task#checkpoint!`** lets a long-running callable task honour Pause and
|
|
63
|
+
Cancel mid-work. A callable task is a single Continuable step, so without it
|
|
64
|
+
a slow `call` ignored Pause until it returned.
|
|
65
|
+
- **`job { concurrency N }`** refuses to start a task while N of its runs are
|
|
66
|
+
already active -- the guard a destructive task needs against a double-clicked
|
|
67
|
+
New Run. Advisory, not a distributed lock.
|
|
68
|
+
|
|
69
|
+
### Fixed
|
|
70
|
+
|
|
71
|
+
- **The task list no longer reorders itself between page loads.** `sort_by` is
|
|
72
|
+
not stable, so two tasks sharing a title -- or any two never-executed tasks
|
|
73
|
+
under `sort=last_run`, which all share the same epoch key -- swapped places
|
|
74
|
+
for no reason. Class name is now the tiebreaker in both orders.
|
|
75
|
+
- **Ghost tasks no longer reach the dashboard.** A class keeps the name it was
|
|
76
|
+
first assigned even after its constant is removed or rebound, and it stays in
|
|
77
|
+
`Task.descendants`, so the registry's sweep re-registered stale copies after
|
|
78
|
+
a development code reload. The sweep now skips any class its own constant no
|
|
79
|
+
longer resolves to.
|
|
80
|
+
|
|
81
|
+
- **Artifact previews no longer crash the run page on non-ASCII content.**
|
|
82
|
+
Two separate encoding faults: `data_blob` is ASCII-8BIT, so interpolating a
|
|
83
|
+
CSV cell holding an accented character into the UTF-8 template raised
|
|
84
|
+
`Encoding::CompatibilityError` at any size; and `byteslice` could cut a
|
|
85
|
+
multibyte character in half, after which matching the line-trim regex raised
|
|
86
|
+
`ArgumentError: invalid byte sequence in UTF-8`. Slices are now transcoded
|
|
87
|
+
and scrubbed before use.
|
|
88
|
+
- **A single-line artifact no longer previews as empty.** The line-trim regex
|
|
89
|
+
matched the entire slice when it contained no newline, blanking the preview
|
|
90
|
+
of any minified or unbroken payload over the cap.
|
|
91
|
+
- **jsonb previews respect the cap on rows with no recorded metadata.**
|
|
92
|
+
`byte_size` fell back to `data_blob`, which is nil for jsonb and text, so
|
|
93
|
+
`preview_truncated?` answered false, the whole document was generated, and
|
|
94
|
+
the byte slice then cut it into unparseable JSON with no truncation notice.
|
|
95
|
+
`byte_size` now measures the column that holds the payload, and jsonb
|
|
96
|
+
trimming keys off the entry count directly.
|
|
97
|
+
- **`database_role` no longer wraps a callable task's `call`.** It scopes the
|
|
98
|
+
collection scan; running a callable task's body -- mostly writes -- under
|
|
99
|
+
`:reading` would fail on a real replica. Callable tasks use
|
|
100
|
+
`Task#with_database_role` for their own reads.
|
|
101
|
+
- **`job_config` is resolved once per run instead of once per record.** It
|
|
102
|
+
allocates a fresh `JobConfig` whenever a task declares no `job` block, so the
|
|
103
|
+
per-record role check meant one throwaway object per processed record.
|
|
104
|
+
- **`Run#resume!` keeps the original failure.** It cleared `error_message` and
|
|
105
|
+
`error_backtrace` before attempting to enqueue, so a resume that itself
|
|
106
|
+
failed destroyed the only record of why the run died. They are now cleared
|
|
107
|
+
only once the job is really queued.
|
|
108
|
+
- **`Run#resume!` raises `MaintenanceOnSteroids::EnqueueFailed`** rather than
|
|
109
|
+
the raw exception, and the controller catches only that -- other errors reach
|
|
110
|
+
the host app's error reporting instead of being redirected as "Run could not
|
|
111
|
+
be enqueued".
|
|
112
|
+
- **Scalar form inputs reject nested structures.** A client posting
|
|
113
|
+
`task_params[name][x]=1` where a string was declared handed the task a
|
|
114
|
+
Parameters object; it is now rejected alongside out-of-range select values.
|
|
115
|
+
|
|
116
|
+
### Changed
|
|
117
|
+
|
|
118
|
+
- The install migration now picks **`jsonb` on PostgreSQL** (`json` elsewhere)
|
|
119
|
+
for the three JSON columns, resolved against the connection at migrate time.
|
|
120
|
+
Existing installs are unaffected; convert with your own migration if you want
|
|
121
|
+
the indexable type.
|
|
122
|
+
- README documents the operational limits that were previously implicit:
|
|
123
|
+
artifact buffering and size caps, pruning, callable-task pause behaviour,
|
|
124
|
+
concurrent runs, the Content Security Policy requirement for live updates,
|
|
125
|
+
and why collections with random UUID primary keys are unsafe to resume.
|
|
126
|
+
|
|
127
|
+
- `pg` moved to an optional bundler group, so the resolved bundle no longer
|
|
128
|
+
depends on `DB` being set in the shell; switching between the SQLite and
|
|
129
|
+
PostgreSQL suites no longer needs a re-install.
|
|
130
|
+
- The Postgres test setup no longer drops every table in the database before
|
|
131
|
+
loading the schema (schema.rb already uses `force: :cascade`), which removed
|
|
132
|
+
a way for concurrent rspec processes to clobber each other.
|
|
133
|
+
- Connection values in the dummy app's `database.yml` are quoted, so a password
|
|
134
|
+
containing `:`, `#`, `%` or `@` no longer produces a YAML syntax error.
|
|
135
|
+
|
|
136
|
+
## 0.1.1 - 2026-08-21 (unreleased)
|
|
137
|
+
|
|
138
|
+
### Security
|
|
139
|
+
|
|
140
|
+
- **The engine now refuses to boot in production with no access control.**
|
|
141
|
+
Every layer is opt-in and the dashboard can start any task against your
|
|
142
|
+
database, so an unconfigured install was silently wide open. HTTP Basic left
|
|
143
|
+
on the shipped `"secret"` password counts as *unconfigured* -- otherwise the
|
|
144
|
+
most dangerous setup would be the quietest one. Hosts that gate the dashboard
|
|
145
|
+
elsewhere (reverse proxy, VPN, middleware) opt out explicitly with
|
|
146
|
+
`config.allow_insecure_dashboard = true`. Outside production this warns
|
|
147
|
+
instead of raising.
|
|
148
|
+
- **`maintenance_on_steroids:install` now generates the initializer**, with
|
|
149
|
+
each access-control layer laid out and commented.
|
|
150
|
+
- **Select inputs are validated server-side.** A `<select>` only constrains the
|
|
151
|
+
browser; a posted value outside the declared `options` is now rejected
|
|
152
|
+
instead of being handed to the task.
|
|
153
|
+
|
|
154
|
+
### Fixed
|
|
155
|
+
|
|
156
|
+
- **Artifacts are no longer lost when a worker is interrupted.**
|
|
157
|
+
`ActiveJob::Continuation::Interrupt` subclasses `Exception`, so it was
|
|
158
|
+
invisible to `RunJob`'s `rescue` and buffered artifact rows written since the
|
|
159
|
+
job started were discarded on every SIGTERM/deploy while the cursor kept
|
|
160
|
+
advancing -- the run still reported `completed`. Flushing now happens in an
|
|
161
|
+
`ensure`, covering every exit path.
|
|
162
|
+
- **`csv` is now a declared runtime dependency.** It left Ruby's default gems
|
|
163
|
+
in 3.4, and `csv_artifact.rb` requires it at load time, so the gem failed to
|
|
164
|
+
boot in host apps on Ruby >= 3.4 that did not list `csv` themselves.
|
|
165
|
+
- **An errored run can be resumed from the dashboard.** A single transient
|
|
166
|
+
failure (deadlock, lock timeout) previously stranded a run mid-write with no
|
|
167
|
+
way to continue. `Run#resumable?` now covers `errored` as well as `paused`,
|
|
168
|
+
and resuming clears the previous error. The record that raised is retried;
|
|
169
|
+
records already past the cursor are not.
|
|
170
|
+
- **`resume_errors_after_advancing` is disabled on `RunJob`.** Continuable was
|
|
171
|
+
re-enqueuing a job that this gem had already marked terminal, so the retry
|
|
172
|
+
fired, no-opped against the guard, and left the run looking retried but never
|
|
173
|
+
advancing.
|
|
174
|
+
- **`Run#resume!` no longer strands a run in `enqueued`.** The status
|
|
175
|
+
compare-and-set happened before `enqueue!`, so a queue outage or a deleted
|
|
176
|
+
task class left the run enqueued with no job behind it (and 500'd the
|
|
177
|
+
controller). Enqueue failures now roll the run back to `errored`.
|
|
178
|
+
- **An interrupted run is marked `enqueued` while it waits to be resumed**, so
|
|
179
|
+
`Run.reap_stale!` no longer mistakes its frozen `updated_at` for a dead
|
|
180
|
+
worker and kills a run that was about to continue.
|
|
181
|
+
- **`RunJob` discards `ActiveRecord::RecordNotFound`** instead of retrying a
|
|
182
|
+
job for a deleted run ~21 times.
|
|
183
|
+
- **Artifact downloads 404 for non-file artifacts** instead of returning a
|
|
184
|
+
200 with a zero-byte `.bin`.
|
|
185
|
+
- **Inline artifact previews are capped** at `Artifact::PREVIEW_BYTES` (256 KB)
|
|
186
|
+
and report truncation. Parsing a multi-hundred-MB export to show its first
|
|
187
|
+
100 rows could exhaust the web process.
|
|
188
|
+
- **Reading an unwritten `:file` artifact no longer re-queries** on every
|
|
189
|
+
access (once per record inside a collection task).
|
|
190
|
+
- **`database_role` actually reaches the replica.** `collection` returns a lazy
|
|
191
|
+
`Relation`, so the documented `with_database_role(:read) { ... }` wrapper
|
|
192
|
+
restored the connection before a single row was fetched and the whole scan
|
|
193
|
+
ran on the primary. Declare it instead -- `job { database_role :reading }` --
|
|
194
|
+
and RunJob holds the role open for the entire scan, stepping back to
|
|
195
|
+
`:writing` for `process` and for its own cursor/progress writes.
|
|
196
|
+
- **`pause!` and `cancel!` are compare-and-set**, like `resume!`. A pause
|
|
197
|
+
clicked as the job completed could overwrite `completed` with `pausing`,
|
|
198
|
+
which nothing but `reap_stale!` would ever clear.
|
|
199
|
+
- **Oversized jsonb previews no longer generate the whole document** before
|
|
200
|
+
slicing it -- the structure is trimmed to `Artifact::PREVIEW_ENTRIES`
|
|
201
|
+
top-level entries first, which was the allocation the byte cap existed to
|
|
202
|
+
avoid.
|
|
203
|
+
|
|
204
|
+
### Changed
|
|
205
|
+
|
|
206
|
+
- The engine now warns at boot when **no** access control is configured
|
|
207
|
+
(`error` level in production). Previously only the partially-configured case
|
|
208
|
+
warned, leaving the fully open dashboard silent.
|
|
209
|
+
- The dashboard stops polling once no run is active, instead of re-rendering
|
|
210
|
+
itself and its aggregate queries every 4 seconds indefinitely.
|
|
211
|
+
- The highlight.js theme stylesheets on the source viewer are SRI-pinned, like
|
|
212
|
+
the script already was.
|
|
213
|
+
- CI runs the suite against PostgreSQL as well as SQLite (`DB=postgres`), so
|
|
214
|
+
`json`/`bytea` behaviour and real row locking are covered. The install
|
|
215
|
+
generator has a spec.
|
|
216
|
+
|
|
217
|
+
## 0.1.0 - 2026-07-25 (unreleased)
|
|
218
|
+
|
|
219
|
+
Initial release.
|
|
220
|
+
|
|
221
|
+
### Added
|
|
222
|
+
|
|
223
|
+
- **Task DSL** built on `MaintenanceOnSteroids::Task`
|
|
224
|
+
- Collection tasks (`collection` + `process`) and callable tasks (`call`)
|
|
225
|
+
- `about` block for title, description and owner metadata
|
|
226
|
+
- `form` block with typed inputs: string, text, integer, float, boolean,
|
|
227
|
+
date, datetime, select and file (`:blob`) uploads
|
|
228
|
+
- `artifact` block for declared outputs: `:jsonb`, `:text`, `:csv`, `:file`
|
|
229
|
+
- `job` block for per-task queue name and priority
|
|
230
|
+
- Lifecycle callbacks: `after_start`, `after_pause`, `after_interrupt`,
|
|
231
|
+
`after_cancel`, `after_complete`, `after_error`
|
|
232
|
+
- `with_database_role` for reading from a replica inside a task
|
|
233
|
+
- **Automatic resumption** via `ActiveJob::Continuable` — a DB-backed cursor is
|
|
234
|
+
written after every processed record, so Sidekiq restarts and pause/resume
|
|
235
|
+
cycles never reprocess work already done
|
|
236
|
+
- **Pause / resume / cancel** with compare-and-set transitions, so concurrent
|
|
237
|
+
requests cannot double-enqueue or overwrite a requested stop
|
|
238
|
+
- **Web dashboard** mounted as a Rails engine
|
|
239
|
+
- Stats overview, active runs, run history with pagination
|
|
240
|
+
- Live progress bars and status badges via JSON polling (no build step,
|
|
241
|
+
no JavaScript dependencies)
|
|
242
|
+
- Estimated time remaining, extrapolated from the current processing rate
|
|
243
|
+
- Task source viewer with optional syntax highlighting (CDN + SRI pinned)
|
|
244
|
+
- Dark and light themes, persisted in `localStorage`
|
|
245
|
+
- **Artifacts** stored in the database: JSON documents, text logs, CSV exports
|
|
246
|
+
and binary blobs, previewable and downloadable from the UI, auto-flushed on
|
|
247
|
+
completion, pause and cancel
|
|
248
|
+
- **Authentication and access control**, layered and off by default: HTTP Basic
|
|
249
|
+
(constant-time comparison), a custom `authentication` hook for
|
|
250
|
+
Devise/Warden/etc., and `verify_access_proc` for role- or IP-based checks
|
|
251
|
+
- **User tracking** — records who triggered each run, with a configurable
|
|
252
|
+
display formatter
|
|
253
|
+
- **Instrumentation** — `ActiveSupport::Notifications` events for every
|
|
254
|
+
lifecycle transition (`enqueued`, `started`, `paused`, `resumed`,
|
|
255
|
+
`cancelled`, `succeeded`, `errored`); a raising subscriber can never corrupt
|
|
256
|
+
a run's status
|
|
257
|
+
- **`Run.reap_stale!`** to recover runs whose worker died mid-execution
|
|
258
|
+
- **Generators** — `maintenance_on_steroids:install` (migration, task
|
|
259
|
+
directory, engine mount) and `maintenance_on_steroids:job` (new task)
|
|
260
|
+
|
|
261
|
+
### Requirements
|
|
262
|
+
|
|
263
|
+
- Rails >= 8.1 (`ActiveJob::Continuable` ships in 8.1)
|
|
264
|
+
- Ruby >= 3.2
|
|
265
|
+
|
|
266
|
+
[Unreleased]: https://github.com/igorkasyanchuk/maintenance_on_steroids/compare/v0.2.0...HEAD
|
|
267
|
+
[0.2.0]: https://github.com/igorkasyanchuk/maintenance_on_steroids/releases/tag/v0.2.0
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Igor Kasyanchuk
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|