@synopackageland/templates 0.9.4 → 0.9.6
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.
- package/README.md +27 -4
- package/package.json +2 -2
- package/templates/_shared/AGENTS.md +95 -18
- package/templates/native-go/backend/THIRD_PARTY_NOTICES.txt +1 -1
- package/templates/native-go/backend/cmd/app/main.go +15 -6
- package/templates/native-go/backend/cmd/background-worker/main.go +5 -1
- package/templates/native-go/backend/go.mod +1 -1
- package/templates/native-go/backend/go.sum +2 -2
- package/templates/native-go/backend/internal/app/background.go +19 -8
- package/templates/native-go/backend/internal/app/background_owner_test.go +2 -2
- package/templates/native-go/backend/internal/app/background_test.go +24 -0
- package/templates/native-go/backend/internal/app/background_work.go +94 -0
- package/templates/native-go/backend/internal/app/background_work_test.go +95 -0
- package/templates/native-go/backend/internal/app/portal.go +49 -4
- package/templates/native-go/backend/internal/app/portal_test.go +128 -0
- package/templates/native-go/backend/internal/app/routes.go +6 -4
- package/templates/native-go/backend/internal/app/uninstall.go +19 -0
- package/templates/native-go/backend/internal/app/uninstall_test.go +33 -0
- package/templates/native-go/dsm-package.yaml +18 -0
- package/templates/native-go-delegate/backend/cmd/background-worker/main.go +1 -1
- package/templates/native-go-delegate/backend/internal/app/delegate_routes_test.go +2 -0
- package/templates/native-go-jobs/backend/internal/app/background_routes_test.go +4 -0
- package/templates/native-go-jobs/frontend/test/background-page.test.ts +21 -0
- package/templates/native-go-vue/frontend/package-lock.json +8 -8
- package/templates/native-go-vue/frontend/package.json +2 -2
- package/templates/native-rust/README.md +381 -0
- package/templates/native-rust/backend/Cargo.lock +125 -0
- package/templates/native-rust/backend/Cargo.toml +24 -0
- package/templates/native-rust/backend/THIRD_PARTY_NOTICES.txt +207 -0
- package/templates/native-rust/backend/permissions.json +5 -0
- package/templates/native-rust/backend/rust-toolchain.toml +4 -0
- package/templates/native-rust/backend/src/app/background/mod.rs +424 -0
- package/templates/native-rust/backend/src/app/background/owner.rs +77 -0
- package/templates/native-rust/backend/src/app/background/owner_tests.rs +174 -0
- package/templates/native-rust/backend/src/app/background/schedule.rs +126 -0
- package/templates/native-rust/backend/src/app/background/schedule_tests.rs +108 -0
- package/templates/native-rust/backend/src/app/background/tests.rs +505 -0
- package/templates/native-rust/backend/src/app/background/work.rs +78 -0
- package/templates/native-rust/backend/src/app/background/work_tests.rs +123 -0
- package/templates/native-rust/backend/src/app/background_routes.rs +22 -0
- package/templates/native-rust/backend/src/app/delegate_config.rs +188 -0
- package/templates/native-rust/backend/src/app/delegate_routes.rs +22 -0
- package/templates/native-rust/backend/src/app/identity.rs +56 -0
- package/templates/native-rust/backend/src/app/migrations.rs +79 -0
- package/templates/native-rust/backend/src/app/mod.rs +30 -0
- package/templates/native-rust/backend/src/app/portal.rs +317 -0
- package/templates/native-rust/backend/src/app/root_routes.rs +23 -0
- package/templates/native-rust/backend/src/app/routes.rs +315 -0
- package/templates/native-rust/backend/src/app/session_files.rs +19 -0
- package/templates/native-rust/backend/src/app/shared_folder.rs +383 -0
- package/templates/native-rust/backend/src/app/test_support.rs +200 -0
- package/templates/native-rust/backend/src/app/uninstall.rs +38 -0
- package/templates/native-rust/backend/src/bin/api.rs +72 -0
- package/templates/native-rust/backend/src/bin/background-worker.rs +37 -0
- package/templates/native-rust/backend/src/build_env.rs +155 -0
- package/templates/native-rust/backend/src/lib.rs +6 -0
- package/templates/native-rust/backend/src/permissions.rs +54 -0
- package/templates/native-rust/backend/src/process.rs +91 -0
- package/templates/native-rust/dsm-package.yaml +89 -0
- package/templates/native-rust/gitignore +2 -0
- package/templates/native-rust/i18n/glossary.json +663 -0
- package/templates/native-rust/i18n/messages/de.json +29 -0
- package/templates/native-rust/i18n/messages/en.json +29 -0
- package/templates/native-rust/i18n/messages/es.json +29 -0
- package/templates/native-rust/i18n/messages/fr.json +29 -0
- package/templates/native-rust/i18n/messages/it.json +29 -0
- package/templates/native-rust/i18n/messages/ja.json +29 -0
- package/templates/native-rust/i18n/messages/ko.json +29 -0
- package/templates/native-rust/i18n/messages/zh-CN.json +29 -0
- package/templates/native-rust/i18n/messages/zh-TW.json +29 -0
- package/templates/native-rust/ui/app.css +45 -0
- package/templates/native-rust/ui/app.js +111 -0
- package/templates/native-rust-delegate/backend/src/app/delegate_routes.rs +92 -0
- package/templates/native-rust-delegate/backend/src/bin/background-worker.rs +52 -0
- package/templates/native-rust-jobs/backend/src/app/background_routes.rs +109 -0
- package/templates/native-rust-root-ops/backend/src/app/root_routes.rs +475 -0
- package/templates/native-rust-vue/README.md +55 -0
- package/templates/native-rust-vue/gitignore +3 -0
package/README.md
CHANGED
|
@@ -11,6 +11,11 @@ the files an App owns; the SDK (`github.com/synopackageland/syno-pkg-land-sdk`,
|
|
|
11
11
|
| `native-go-jobs` | Background job routes in `internal/app` and their page test |
|
|
12
12
|
| `native-go-delegate` | Delegate routes, the delegate background worker, the delegate `CapabilityPanels.vue` |
|
|
13
13
|
| `native-go-root-ops` | Executor routes in `internal/app` and an example `root/whoami.sh` |
|
|
14
|
+
| `native-rust` | Base: `dsm-package.yaml` with `backend.kind: rust`, Cargo package `app` (`backend/src/bin/{api,background-worker}.rs`, `backend/src/app`, `backend/permissions.json`, committed `Cargo.lock`, `rust-toolchain.toml`), plain `ui/`, i18n |
|
|
15
|
+
| `native-rust-vue` | Only the Vue README and gitignore; the shell itself is `native-go-vue`'s `frontend/` |
|
|
16
|
+
| `native-rust-jobs` | `backend/src/app/background_routes.rs`: the background job routes and their tests |
|
|
17
|
+
| `native-rust-delegate` | `delegate_routes.rs` and the delegate `background-worker.rs` |
|
|
18
|
+
| `native-rust-root-ops` | `root_routes.rs`: the executor routes and their tests |
|
|
14
19
|
| `binary` | A prebuilt static CGI (`example/main.go`) with a plain UI |
|
|
15
20
|
| `compose` | Container Manager project (`compose.yaml`, `synology-app.yaml`) |
|
|
16
21
|
| `_shared` | Agent guides, template license, Go license texts, third-party notice writer |
|
|
@@ -21,9 +26,22 @@ An overlay may replace `internal/app/*_none.go` (the CLI deletes the matching
|
|
|
21
26
|
The placeholders `__PACKAGE__` (also the Go module path), `__DISPLAY_NAME__` and
|
|
22
27
|
`__BACKGROUND_ENABLED__` are substituted by `init`.
|
|
23
28
|
|
|
24
|
-
|
|
29
|
+
Rust has no build tags, so the `native-rust-*` overlays overwrite same-named
|
|
30
|
+
base files (`background_routes.rs`, `delegate_routes.rs`, `root_routes.rs`,
|
|
31
|
+
`bin/background-worker.rs`) and nothing is removed. They carry only the
|
|
32
|
+
backend. A Rust project borrows the native-go overlays' non-backend files: the
|
|
33
|
+
Vue shell from `native-go-vue` (then `native-rust-vue`), `frontend/` of
|
|
34
|
+
`native-go-jobs` and `native-go-delegate`, and `root/whoami.sh` of
|
|
35
|
+
`native-go-root-ops`. A delegate project without root-ops also takes
|
|
36
|
+
`native-rust-root-ops`'s `root_routes.rs`, the executor shell Go borrows as
|
|
37
|
+
`executor.go`. `scripts/assemble.sh --language rust` does exactly this.
|
|
38
|
+
|
|
39
|
+
`backend/go.mod` requires `github.com/synopackageland/syno-pkg-land-sdk v0.9.7`; that line is
|
|
25
40
|
the single source of the SDK version and `frontend/package.json` pins
|
|
26
|
-
`@synopackageland/sdk` to the same version.
|
|
41
|
+
`@synopackageland/sdk` to the same version. A Rust project takes it from the
|
|
42
|
+
`tag` of the `syno-pkg-land-sdk` git dependency in `backend/Cargo.toml`, which
|
|
43
|
+
`Cargo.lock` repeats; build-time values reach it through the contract's
|
|
44
|
+
`SYNOPKGLAND_*` environment variables.
|
|
27
45
|
|
|
28
46
|
## Notice writer
|
|
29
47
|
|
|
@@ -45,7 +63,8 @@ and `UI-THIRD_PARTY.md` in the output directory; other files there stay.
|
|
|
45
63
|
export GOPRIVATE='github.com/synopackageland/*' # the SDK repository is private
|
|
46
64
|
export SYNOPKGLAND_SDK_DIR=~/Public/synopackageland/sdk # optional sdk checkout: Go module at the root, ts/
|
|
47
65
|
scripts/assemble.sh /tmp/app vue delegate # one combination, as init lays it out
|
|
48
|
-
scripts/
|
|
66
|
+
scripts/assemble.sh --language rust /tmp/app vue delegate # the same, for native-rust
|
|
67
|
+
scripts/gate.sh # layout, every combination (Go and Rust), binary, npm pack
|
|
49
68
|
```
|
|
50
69
|
|
|
51
70
|
Without `SYNOPKGLAND_SDK_DIR` the combinations use the released SDK that
|
|
@@ -53,7 +72,11 @@ Without `SYNOPKGLAND_SDK_DIR` the combinations use the released SDK that
|
|
|
53
72
|
`replace github.com/synopackageland/syno-pkg-land-sdk => $SYNOPKGLAND_SDK_DIR`
|
|
54
73
|
and installs `$SYNOPKGLAND_SDK_DIR/ts` as an `npm pack` tarball (not a link, so Vue
|
|
55
74
|
resolves once). With `SYNOPKGLAND_UI_TARBALL` set it installs that tarball as
|
|
56
|
-
`@synopackageland/ui`, for a UI version not yet on npm.
|
|
75
|
+
`@synopackageland/ui`, for a UI version not yet on npm. The Rust gate runs `cargo fmt --check`, `cargo clippy -D warnings`, `cargo test --locked`
|
|
76
|
+
and a target clippy for the three DSM targets per combination; `rustup` installs the
|
|
77
|
+
toolchain `backend/rust-toolchain.toml` pins. With `SYNOPKGLAND_SDK_DIR`, `scripts/rust-env.sh`
|
|
78
|
+
(sourced by the gate) makes cargo fetch the SDK's pinned tag from that checkout instead of
|
|
79
|
+
GitHub, leaving the Cargo files as shipped. CI is not available; `scripts/gate.sh` is the gate.
|
|
57
80
|
|
|
58
81
|
SDK usage, protocol and API documentation: <https://github.com/synopackageland/syno-pkg-land-sdk>.
|
|
59
82
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synopackageland/templates",
|
|
3
|
-
"version": "0.9.
|
|
4
|
-
"description": "Project templates for synopkgland init: native-go (plain or Vue, with jobs, delegate and root-ops overlays), binary and compose.",
|
|
3
|
+
"version": "0.9.6",
|
|
4
|
+
"description": "Project templates for synopkgland init: native-go and native-rust (plain or Vue, with jobs, delegate and root-ops overlays), binary and compose.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {"type": "git", "url": "git+https://github.com/synopackageland/syno-pkg-land-templates.git"},
|
|
7
7
|
"exports": {
|
|
@@ -17,6 +17,9 @@ Only a confirmed refusal (signed out, expired session, not an administrator)
|
|
|
17
17
|
shows the sign-in page or a `@synopackageland/ui` Dialog; loading text inside a
|
|
18
18
|
section is fine.
|
|
19
19
|
|
|
20
|
+
Everything below holds for every native backend language; the backend
|
|
21
|
+
sections at the end name each language's files, types and test commands.
|
|
22
|
+
|
|
20
23
|
From the project root, run `synopkgland check --json`,
|
|
21
24
|
`synopkgland build --json`, then `synopkgland inspect <spk> --json` on the emitted SPK.
|
|
22
25
|
For Vue, first run `npm ci`, `npm test` and `npm run build` in frontend.
|
|
@@ -32,7 +35,7 @@ access. One CGI check covers every route; failures refuse; changes apply within
|
|
|
32
35
|
30 seconds. Never re-implement AppPriv rules in the App.
|
|
33
36
|
Mark routes ordinary users call `authenticated` in permissions.json, keep whole
|
|
34
37
|
admin routes at the default, guard global settings inside shared routes with
|
|
35
|
-
|
|
38
|
+
the SDK's require-admin check, and hide admin controls with `ctx.user?.isAdmin`
|
|
36
39
|
(UI only, never the authority). Example: users read the greeting; only admins
|
|
37
40
|
call `greeting.set`.
|
|
38
41
|
|
|
@@ -51,9 +54,46 @@ admin's session) or Control Panel > Login Portal > Applications. Ordinary users
|
|
|
51
54
|
get the link from the backend's read-only `portal.status`, else "ask an
|
|
52
55
|
administrator". Never write `AppPortal.json` or `@appconf/<package>`, never add
|
|
53
56
|
a backend route that sets the alias, and never relay QuickConnect responses or
|
|
54
|
-
build QuickConnect links. A portal needs os_min_ver 7.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
+
build QuickConnect links. A portal needs os_min_ver 7.1-42661 or later, and
|
|
58
|
+
`add portal` raises a lower one. Below DSM 7.2 the executor registers the
|
|
59
|
+
entry: there is no alias wizard step, and an administrator enables the entry
|
|
60
|
+
from the portal panel (`ctx.portal.register()`), which the executor's approved
|
|
61
|
+
`portal.register` operation carries out. From 7.2 the SPK registers it as a
|
|
62
|
+
DSM resource. Beyond that, support is detected (SYNO.API.Info), not
|
|
63
|
+
version-gated.
|
|
64
|
+
|
|
65
|
+
## Background jobs (`jobs`)
|
|
66
|
+
|
|
67
|
+
The worker runs the App's Work for one claimed job at a time, as the package
|
|
68
|
+
account. The Work receives the job's Task: it reads the private input copy,
|
|
69
|
+
reports progress (`done`/`total`, `bytes`/`totalBytes`, `item`) and saves a
|
|
70
|
+
checkpoint, at most 16 KiB of text, only once the work it describes is done.
|
|
71
|
+
Owners pause and resume their jobs (`bg.jobs.pause`, `bg.jobs.resume`; the
|
|
72
|
+
SDK BackgroundPanel shows the buttons): a queued job pauses at once, a running
|
|
73
|
+
one at its next checkpoint poll. A package stop pauses the running job too, and
|
|
74
|
+
the next start resumes it. A resumed job runs again from its last checkpoint,
|
|
75
|
+
or from the start when it saved none, so a Work must be safe to repeat from any
|
|
76
|
+
checkpoint it saved. Delegated jobs cannot pause, and the pause routes answer
|
|
77
|
+
`conflict` for them. The template's Work hashes the input, reports bytes read
|
|
78
|
+
and checkpoints the hash state every 64 KiB; replace it with the App's own work.
|
|
79
|
+
|
|
80
|
+
## Uninstall hook (`uninstall`)
|
|
81
|
+
|
|
82
|
+
Declare `uninstall.purge` in dsm-package.yaml (the template ships it
|
|
83
|
+
commented) to run a fixed package executable on a real uninstall (not an
|
|
84
|
+
upgrade), as the package account, before the executor is removed. It gets
|
|
85
|
+
`SYNOPKGLAND_DELETE_STATE=0|1` and `SYNOPKGLAND_REMOVE_DATA=0|1` from the
|
|
86
|
+
uninstall wizard and nothing else from the environment. Release what the App
|
|
87
|
+
holds outside the package on every uninstall; delete the state listed in
|
|
88
|
+
`uninstall.state.items` (each key needs `uninstall.state.<key>` in
|
|
89
|
+
i18n/messages) only with `SYNOPKGLAND_DELETE_STATE=1`, and keep what a
|
|
90
|
+
reinstall can reuse otherwise. A failure or timeout (2–120 s, default 60) is
|
|
91
|
+
logged to syslog tag `synopkgland-uninstall` with output in
|
|
92
|
+
`var/uninstall-hook.log` and never blocks the uninstall. `purge.root: <op>`
|
|
93
|
+
(rootOps only) adds a root step `root/<op>.sh [delete-state]` that runs only
|
|
94
|
+
while the administrator's approval of that exact policy stands; sessions
|
|
95
|
+
cannot call it through `root.run`. The template backend's `uninstall` mode
|
|
96
|
+
deletes the background job spool when asked.
|
|
57
97
|
|
|
58
98
|
## Permanent destination integration
|
|
59
99
|
|
|
@@ -80,8 +120,8 @@ a receipt. Cancel by the same reference; revoke by grant ID. Never resubmit
|
|
|
80
120
|
uncertain/interrupted work. Admin disable is protected and revokes grants;
|
|
81
121
|
enable/mailbox start does not resurrect them. Keep IDs, never session credentials.
|
|
82
122
|
|
|
83
|
-
App-owned daemon completion and scheduled CSV export call configured
|
|
84
|
-
|
|
123
|
+
App-owned daemon completion and scheduled CSV export call the configured
|
|
124
|
+
delegate service's move-from-staging with `delegation_id`, stable `job_id`, private relative
|
|
85
125
|
`staging`, `name`, `size`. No owner/UID selector. Bounds: 16 GiB/file, queued16,
|
|
86
126
|
active1/package, 24-hour attempt, 5-second revalidation, 10 MiB operation records,
|
|
87
127
|
50 MiB audit. Scope is a usage restriction, not hard subtree confinement.
|
|
@@ -93,7 +133,7 @@ Admission recovery (per job): on `EXECUTOR_UNAVAILABLE`, `EXECUTOR_TIMEOUT`,
|
|
|
93
133
|
`EXECUTOR_PROTOCOL`, `canceled`, `EXECUTOR_PAUSED`, `queue-full`,
|
|
94
134
|
`STORAGE_ERROR`, `ADMISSION_PENDING`, `jobs_busy` or `jobs_unavailable` (the
|
|
95
135
|
package journal was busy or failed transiently; a caller deadline or cancel
|
|
96
|
-
spent waiting for it is `canceled`), call
|
|
136
|
+
spent waiting for it is `canceled`), call move-from-staging again later with
|
|
97
137
|
the same exact params. It queries the original reference first and re-sends
|
|
98
138
|
admission only when the executor does not know it and the pinned source is
|
|
99
139
|
unchanged; executor tuple deduplication keeps one operation, so a lost reply or a
|
|
@@ -105,11 +145,11 @@ retry: `APP_DENIED`, `DELEGATION_DENIED`, `operation-conflict`, `disabled`,
|
|
|
105
145
|
spool first), and `operation-not-found` for an accepted operation.
|
|
106
146
|
Never change job/grant IDs, delete mirrors or call the socket directly to retry.
|
|
107
147
|
|
|
108
|
-
Cancellation (per job):
|
|
148
|
+
Cancellation (per job): the service's cancel-operation / `delegate.operations.cancel` with the
|
|
109
149
|
original ref is idempotent; repeat it with the same ref after any retryable
|
|
110
150
|
error or a lost reply. A reply `queued`→`canceled` is final at once. A copy in
|
|
111
151
|
flight answers `copying` with code `CANCEL_REQUESTED`: the cancel is accepted and
|
|
112
|
-
durable, not finished; poll
|
|
152
|
+
durable, not finished; poll the operation status on the same ref until terminal
|
|
113
153
|
(repeating the cancel is harmless). `canceled` (`OPERATION_CANCELED`) guarantees the writer
|
|
114
154
|
stopped, its part file is removed, nothing was published and staging is kept; it
|
|
115
155
|
stays canceled across executor crash, package stop/start and logout. `committed`
|
|
@@ -118,7 +158,7 @@ the app record may be dropped. `uncertain` means a publish attempt failed before
|
|
|
118
158
|
the cancel; keep querying. A cancel of a ref the executor has not admitted yet
|
|
119
159
|
(reserved/sent mirror) returns `canceled` and leaves a tombstone, so a late or
|
|
120
160
|
recovered admission of that ref returns `canceled` and never copies; a producer
|
|
121
|
-
cancel is also written to the mirror first, so
|
|
161
|
+
cancel is also written to the mirror first, so move-from-staging and the worker
|
|
122
162
|
sweep forward the cancel instead of admitting it.
|
|
123
163
|
|
|
124
164
|
|
|
@@ -142,14 +182,51 @@ sweep forward the cancel instead of admitting it.
|
|
|
142
182
|
sdk.destination('save')` returns typed `{key, selection, destination, status}`.
|
|
143
183
|
`selection` contains `{scope, root, generation}`; `destination` is this caller's
|
|
144
184
|
grant or null. Handle active/missing/reselection-required/needs-scope-confirmation/disabled.
|
|
145
|
-
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
185
|
+
- Backend: the service's destination lookup verifies the fresh DSM session and
|
|
186
|
+
username+UID. Capture its active grant ID into the App job. The offline
|
|
187
|
+
producer uses the existing move-from-staging; no user/UID selector, cookie,
|
|
188
|
+
token, or browser path cache. Executor revalidates at use/publication.
|
|
149
189
|
- The generated Background saves page includes a working **Save example file**
|
|
150
|
-
(`sdk.exportExample({key, job_id})`, `delegate.example.export`).
|
|
151
|
-
|
|
152
|
-
repeats the original request on repeated IDs. Review
|
|
153
|
-
adapt a real producer; show Saved only from a protected committed
|
|
190
|
+
(`sdk.exportExample({key, job_id})`, `delegate.example.export`). The SDK's
|
|
191
|
+
export-example creates a private JSON file, calls move-from-staging once, and
|
|
192
|
+
repeats the original request on repeated IDs. Review it in the SDK's delegate
|
|
193
|
+
package to adapt a real producer; show Saved only from a protected committed
|
|
194
|
+
receipt.
|
|
154
195
|
- Wizard support is measured on vt-nas DSM 7.4 via a session-only SSH local
|
|
155
196
|
forward. This is not proof of other DSM releases or full writer/reboot behavior.
|
|
197
|
+
|
|
198
|
+
## Go backend
|
|
199
|
+
|
|
200
|
+
- Routes: `backend/internal/app` (`routes.go` registers them); App
|
|
201
|
+
permissions: `backend/internal/api/permissions.json`. Test with
|
|
202
|
+
`(cd backend && go test ./...)`, then `go vet ./...` and `gofmt -l .`.
|
|
203
|
+
- Admin guard inside a shared route: `api.RequireAdmin(caller)`.
|
|
204
|
+
- Jobs: `background.Work` is `func(ctx, *background.Task) (background.Result,
|
|
205
|
+
error)`; call `t.Report(background.Progress{...})` and `t.Checkpoint(state)`,
|
|
206
|
+
and resume from `t.Job.Checkpoint`. The template's Work is
|
|
207
|
+
`app.HashWork` (`background_work.go`), set as `Runner.Work` in
|
|
208
|
+
`cmd/background-worker` and as `delegate.Service.Work` with delegate.
|
|
209
|
+
- Uninstall hook: `app.Uninstall(dataDir, deleteState)` (`uninstall.go`),
|
|
210
|
+
reached by `api.cgi uninstall`.
|
|
211
|
+
- Delegate: `delegate.Service.Destination(ctx, caller, request, key)`,
|
|
212
|
+
`Service.MoveFromStaging`, `Service.CancelOperation`,
|
|
213
|
+
`Service.OperationStatus`, `Service.ExportExample` (the SDK's
|
|
214
|
+
`delegate/example.go`).
|
|
215
|
+
|
|
216
|
+
## Rust backend
|
|
217
|
+
|
|
218
|
+
- Routes: `backend/src/app/routes.rs`; App permissions:
|
|
219
|
+
`backend/permissions.json`. Test with `(cd backend && cargo test --locked)`,
|
|
220
|
+
then `cargo clippy --locked --all-targets -- -D warnings` and
|
|
221
|
+
`cargo fmt --check`.
|
|
222
|
+
- Admin guard inside a shared route: `api::require_admin(caller)`.
|
|
223
|
+
- Jobs: `background::Work` is `Arc<dyn Fn(&Context, &mut Task<'_>) ->
|
|
224
|
+
Result<JobResult, BoxError>>`; call `t.report(Progress {..})` and
|
|
225
|
+
`t.checkpoint(state)`, and resume from `t.job.checkpoint`. The template's
|
|
226
|
+
Work is `app::hash_work` (`app/background/work.rs`, hash state through
|
|
227
|
+
`sha256::Sha256::save`/`restore`), set as `Runner.work` in
|
|
228
|
+
`bin/background-worker.rs` and as `delegate::Service.work` with delegate.
|
|
229
|
+
- Uninstall hook: `app::uninstall(data_dir, delete_state)`
|
|
230
|
+
(`app/uninstall.rs`), reached by `api.cgi uninstall`.
|
|
231
|
+
- Delegate: `Service::destination`, `move_from_staging`, `cancel_operation`,
|
|
232
|
+
`operation_status`, `export_example` (the SDK's `delegate/example.rs`).
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Command app is the package backend. DSM runs it as a CGI (ui/cgi/api.cgi in
|
|
2
|
-
// the package) under the package account, once per request
|
|
2
|
+
// the package) under the package account, once per request, and as the
|
|
3
|
+
// uninstall hook (`api.cgi uninstall`) when dsm-package.yaml declares one.
|
|
3
4
|
//
|
|
4
5
|
//line __PACKAGE__/cmd/app/main.go:1
|
|
5
6
|
package main
|
|
@@ -36,13 +37,20 @@ var (
|
|
|
36
37
|
identity = ""
|
|
37
38
|
// dsm-package.yaml entry: "admin-only" or "app-privilege"; empty keeps the route policy alone.
|
|
38
39
|
entry = ""
|
|
39
|
-
// "on" when dsm-package.yaml declares portal: serves portal.status
|
|
40
|
+
// "on" when dsm-package.yaml declares portal: serves portal.status;
|
|
41
|
+
// "executor" below DSM 7.2, where the executor registers the entry.
|
|
40
42
|
portal = ""
|
|
41
43
|
)
|
|
42
44
|
|
|
43
45
|
func main() {
|
|
44
46
|
log.SetFlags(0)
|
|
45
47
|
log.SetPrefix(packageID + ": ")
|
|
48
|
+
if os.Getenv("GATEWAY_INTERFACE") == "" && len(os.Args) == 2 && os.Args[1] == "uninstall" {
|
|
49
|
+
if err := app.Uninstall("/var/packages/"+packageID+"/var", os.Getenv("SYNOPKGLAND_DELETE_STATE") == "1"); err != nil {
|
|
50
|
+
log.Fatal(err)
|
|
51
|
+
}
|
|
52
|
+
return
|
|
53
|
+
}
|
|
46
54
|
if os.Getenv("GATEWAY_INTERFACE") == "" {
|
|
47
55
|
fmt.Fprintf(os.Stderr, "%s %s: this program runs as the package's DSM CGI; open the app from the DSM main menu.\n", packageID, version)
|
|
48
56
|
os.Exit(2)
|
|
@@ -53,10 +61,11 @@ func main() {
|
|
|
53
61
|
DelegateDestinationsJSON: delegateDestinationsJSON,
|
|
54
62
|
DelegateLifetime: delegateLifetime,
|
|
55
63
|
ExecutorDigest: executorDigest, PolicyDigest: policyDigest,
|
|
56
|
-
PackageID:
|
|
57
|
-
Version:
|
|
58
|
-
DataDir:
|
|
59
|
-
Portal:
|
|
64
|
+
PackageID: packageID,
|
|
65
|
+
Version: version,
|
|
66
|
+
DataDir: "/var/packages/" + packageID + "/var",
|
|
67
|
+
Portal: portal != "",
|
|
68
|
+
PortalExecutor: portal == "executor",
|
|
60
69
|
}
|
|
61
70
|
if _, err := app.ParseDelegateConfig(cfg); err != nil {
|
|
62
71
|
log.Fatal(err)
|
|
@@ -13,6 +13,8 @@ import (
|
|
|
13
13
|
"syscall"
|
|
14
14
|
|
|
15
15
|
"github.com/synopackageland/syno-pkg-land-sdk/background"
|
|
16
|
+
|
|
17
|
+
"__PACKAGE__/internal/app"
|
|
16
18
|
)
|
|
17
19
|
|
|
18
20
|
// Set by `synopkgland build-spk` through -ldflags -X.
|
|
@@ -32,7 +34,9 @@ func main() {
|
|
|
32
34
|
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT)
|
|
33
35
|
defer stop()
|
|
34
36
|
log.Printf("started as uid %s", strconv.Itoa(info.UID))
|
|
35
|
-
|
|
37
|
+
runner := background.NewRunner("/var/packages/"+packageID+"/var/background", info)
|
|
38
|
+
runner.Work = app.HashWork
|
|
39
|
+
if err := runner.Run(ctx); err != nil {
|
|
36
40
|
log.Fatal(err)
|
|
37
41
|
}
|
|
38
42
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
github.com/synopackageland/syno-pkg-land-sdk v0.9.
|
|
2
|
-
github.com/synopackageland/syno-pkg-land-sdk v0.9.
|
|
1
|
+
github.com/synopackageland/syno-pkg-land-sdk v0.9.7 h1:i8Zeowz7pLu3dI7v5/RijCw5OwMoZdMgzUoV/sD68n8=
|
|
2
|
+
github.com/synopackageland/syno-pkg-land-sdk v0.9.7/go.mod h1:hlsvaVuH2wsAoR8jzI1Rg/NeejQSFpAaN6zpIWsVbjI=
|
|
3
3
|
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
|
|
4
4
|
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
|
@@ -65,6 +65,8 @@ func newBackgroundRoutes(store *background.Store, files backgroundFiles) []api.R
|
|
|
65
65
|
api.HandleRequest("bg.jobs.list", b.owner(b.list)),
|
|
66
66
|
api.HandleRequest("bg.jobs.get", b.owner(b.get)),
|
|
67
67
|
api.HandleRequest("bg.jobs.cancel", b.owner(b.cancel)),
|
|
68
|
+
api.HandleRequest("bg.jobs.pause", b.owner(b.pause)),
|
|
69
|
+
api.HandleRequest("bg.jobs.resume", b.owner(b.resume)),
|
|
68
70
|
api.HandleRequest("bg.jobs.result", b.owner(b.result)),
|
|
69
71
|
}
|
|
70
72
|
}
|
|
@@ -175,20 +177,28 @@ func (b backgroundAPI) list(ctx context.Context, _ api.Caller, _ *http.Request,
|
|
|
175
177
|
}
|
|
176
178
|
|
|
177
179
|
func (b backgroundAPI) get(ctx context.Context, _ api.Caller, _ *http.Request, params json.RawMessage) (any, error) {
|
|
178
|
-
|
|
179
|
-
if err := decodeParams(params, &in); err != nil {
|
|
180
|
-
return nil, err
|
|
181
|
-
}
|
|
182
|
-
job, err := b.store.GetFor(ctx, in.ID, accessFrom(ctx))
|
|
183
|
-
return jobOrError(job, err)
|
|
180
|
+
return b.byID(ctx, params, b.store.GetFor)
|
|
184
181
|
}
|
|
185
182
|
|
|
186
183
|
func (b backgroundAPI) cancel(ctx context.Context, _ api.Caller, _ *http.Request, params json.RawMessage) (any, error) {
|
|
184
|
+
return b.byID(ctx, params, b.store.CancelFor)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// pause holds a queued job at once; a running job pauses at its next checkpoint.
|
|
188
|
+
func (b backgroundAPI) pause(ctx context.Context, _ api.Caller, _ *http.Request, params json.RawMessage) (any, error) {
|
|
189
|
+
return b.byID(ctx, params, b.store.PauseFor)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
func (b backgroundAPI) resume(ctx context.Context, _ api.Caller, _ *http.Request, params json.RawMessage) (any, error) {
|
|
193
|
+
return b.byID(ctx, params, b.store.ResumeFor)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
func (b backgroundAPI) byID(ctx context.Context, params json.RawMessage, act func(context.Context, string, background.Access) (background.Job, error)) (any, error) {
|
|
187
197
|
var in idParams
|
|
188
198
|
if err := decodeParams(params, &in); err != nil {
|
|
189
199
|
return nil, err
|
|
190
200
|
}
|
|
191
|
-
job, err :=
|
|
201
|
+
job, err := act(ctx, in.ID, accessFrom(ctx))
|
|
192
202
|
return jobOrError(job, err)
|
|
193
203
|
}
|
|
194
204
|
|
|
@@ -239,7 +249,8 @@ func backgroundError(err error) error {
|
|
|
239
249
|
return &api.Error{Code: "queue-full", Message: "the background queue is full"}
|
|
240
250
|
case errors.Is(err, background.ErrQuota):
|
|
241
251
|
return &api.Error{Code: "quota-exceeded", Message: "the background spool is full"}
|
|
242
|
-
case errors.Is(err, background.ErrKeyConflict), errors.Is(err, background.ErrNotCancelable)
|
|
252
|
+
case errors.Is(err, background.ErrKeyConflict), errors.Is(err, background.ErrNotCancelable),
|
|
253
|
+
errors.Is(err, background.ErrNotPausable):
|
|
243
254
|
return &api.Error{Code: "conflict", Message: "the request conflicts with the job's current state"}
|
|
244
255
|
case errors.Is(err, background.ErrNotFound):
|
|
245
256
|
return &api.Error{Code: "not_found", Message: "job not found"}
|
|
@@ -52,7 +52,7 @@ func TestBackgroundOwnersAreIsolatedIncludingKeysAndCounts(t *testing.T) {
|
|
|
52
52
|
t.Fatalf("status leaks: %v", out)
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
|
-
for _, method := range []string{"bg.jobs.get", "bg.jobs.cancel", "bg.jobs.result"} {
|
|
55
|
+
for _, method := range []string{"bg.jobs.get", "bg.jobs.cancel", "bg.jobs.pause", "bg.jobs.resume", "bg.jobs.result"} {
|
|
56
56
|
for _, id := range []string{a, c, strings.Repeat("0", 32)} {
|
|
57
57
|
if code, out := e.call(bob, method, `{"id":"`+id+`"}`); code != "not_found" {
|
|
58
58
|
t.Fatalf("%s %s: %s %v", method, id, code, out)
|
|
@@ -144,7 +144,7 @@ func TestFreshOrdinaryUserJobsNeedNoAdminSetup(t *testing.T) {
|
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
146
|
id := ownerSubmit(t, e, bob, "fresh")
|
|
147
|
-
for _, method := range []string{"bg.jobs.get", "bg.jobs.cancel"} {
|
|
147
|
+
for _, method := range []string{"bg.jobs.get", "bg.jobs.cancel", "bg.jobs.pause", "bg.jobs.resume"} {
|
|
148
148
|
if code, _ := e.call(carol, method, `{"id":"`+id+`"}`); code != "not_found" {
|
|
149
149
|
t.Fatalf("other owner %s: %s", method, code)
|
|
150
150
|
}
|
|
@@ -91,6 +91,8 @@ var bgSample = map[string]string{
|
|
|
91
91
|
"bg.jobs.list": `{}`,
|
|
92
92
|
"bg.jobs.get": `{"id":"` + strings.Repeat("a", 32) + `"}`,
|
|
93
93
|
"bg.jobs.cancel": `{"id":"` + strings.Repeat("a", 32) + `"}`,
|
|
94
|
+
"bg.jobs.pause": `{"id":"` + strings.Repeat("a", 32) + `"}`,
|
|
95
|
+
"bg.jobs.resume": `{"id":"` + strings.Repeat("a", 32) + `"}`,
|
|
94
96
|
"bg.jobs.result": `{"id":"` + strings.Repeat("a", 32) + `"}`,
|
|
95
97
|
}
|
|
96
98
|
|
|
@@ -262,6 +264,28 @@ func TestBackgroundCancelQueuedJob(t *testing.T) {
|
|
|
262
264
|
}
|
|
263
265
|
}
|
|
264
266
|
|
|
267
|
+
func TestBackgroundPauseAndResumeQueuedJob(t *testing.T) {
|
|
268
|
+
e := newBgEnv(t)
|
|
269
|
+
_, out := e.call(alice, "bg.jobs.submit", `{"key":"k1","folder":"/share","relative":"a.txt"}`)
|
|
270
|
+
idp := `{"id":"` + data(out)["job"].(map[string]any)["id"].(string) + `"}`
|
|
271
|
+
for method, state := range map[string]string{"bg.jobs.pause": "paused", "bg.jobs.resume": "queued"} {
|
|
272
|
+
if code, out := e.call(alice, method, idp); code != "" || data(out)["job"].(map[string]any)["state"] != state {
|
|
273
|
+
t.Fatalf("%s: %s %v", method, code, out)
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if code, _ := e.call(alice, "bg.jobs.cancel", idp); code != "" {
|
|
277
|
+
t.Fatal(code)
|
|
278
|
+
}
|
|
279
|
+
for _, m := range []string{"bg.jobs.pause", "bg.jobs.resume"} {
|
|
280
|
+
if code, _ := e.call(alice, m, idp); code != "conflict" {
|
|
281
|
+
t.Fatalf("%s after cancel: %q", m, code)
|
|
282
|
+
}
|
|
283
|
+
if code, _ := e.call(alice, m, `{"id":"`+strings.Repeat("0", 32)+`"}`); code != "not_found" {
|
|
284
|
+
t.Fatalf("%s unknown id: %q", m, code)
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
265
289
|
func TestBackgroundWorkerAliveExpires(t *testing.T) {
|
|
266
290
|
if workerAlive(background.WorkerInfo{At: time.Now().Add(-time.Minute)}, time.Now()) || !workerAlive(background.WorkerInfo{At: time.Now()}, time.Now()) {
|
|
267
291
|
t.Fatal("a stale heartbeat is not alive")
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
package app
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"crypto/sha256"
|
|
6
|
+
"encoding"
|
|
7
|
+
"encoding/hex"
|
|
8
|
+
"encoding/json"
|
|
9
|
+
"errors"
|
|
10
|
+
"fmt"
|
|
11
|
+
"hash"
|
|
12
|
+
"io"
|
|
13
|
+
|
|
14
|
+
"github.com/synopackageland/syno-pkg-land-sdk/background"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
// hashCheckpointEvery spaces HashWork's checkpoints; a resumed job repeats at
|
|
18
|
+
// most this much hashing. A Work over larger input saves less often.
|
|
19
|
+
const hashCheckpointEvery = 64 << 10
|
|
20
|
+
|
|
21
|
+
// hashState is HashWork's checkpoint: the bytes already hashed and the hash
|
|
22
|
+
// state after them.
|
|
23
|
+
type hashState struct {
|
|
24
|
+
Offset int64 `json:"offset"`
|
|
25
|
+
State []byte `json:"state"`
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// HashWork is the App's sha256-v1 Work: it reports progress as it reads and
|
|
29
|
+
// checkpoints the hash state, so a job paused by its owner or by a package
|
|
30
|
+
// stop resumes where it left off instead of from the start.
|
|
31
|
+
func HashWork(ctx context.Context, t *background.Task) (background.Result, error) {
|
|
32
|
+
h, done, err := resumeHash(t.Job.Checkpoint, t.Input)
|
|
33
|
+
if err != nil {
|
|
34
|
+
return background.Result{}, err
|
|
35
|
+
}
|
|
36
|
+
total, saved := int64(t.Job.InputBytes), done
|
|
37
|
+
buf := make([]byte, 32<<10)
|
|
38
|
+
for {
|
|
39
|
+
if err := ctx.Err(); err != nil {
|
|
40
|
+
return background.Result{}, err
|
|
41
|
+
}
|
|
42
|
+
n, err := t.Input.Read(buf)
|
|
43
|
+
h.Write(buf[:n])
|
|
44
|
+
done += int64(n)
|
|
45
|
+
t.Report(background.Progress{Bytes: done, TotalBytes: total})
|
|
46
|
+
if errors.Is(err, io.EOF) {
|
|
47
|
+
return background.Result{SHA256: hex.EncodeToString(h.Sum(nil)), Bytes: int(done)}, nil
|
|
48
|
+
}
|
|
49
|
+
if err != nil {
|
|
50
|
+
return background.Result{}, err
|
|
51
|
+
}
|
|
52
|
+
if done-saved >= hashCheckpointEvery {
|
|
53
|
+
state, err := saveHash(h, done)
|
|
54
|
+
if err == nil {
|
|
55
|
+
err = t.Checkpoint(state)
|
|
56
|
+
}
|
|
57
|
+
if err != nil {
|
|
58
|
+
return background.Result{}, err
|
|
59
|
+
}
|
|
60
|
+
saved = done
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
func saveHash(h hash.Hash, offset int64) (string, error) {
|
|
66
|
+
state, err := h.(encoding.BinaryMarshaler).MarshalBinary()
|
|
67
|
+
if err != nil {
|
|
68
|
+
return "", err
|
|
69
|
+
}
|
|
70
|
+
out, err := json.Marshal(hashState{Offset: offset, State: state})
|
|
71
|
+
return string(out), err
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// resumeHash continues the hash a checkpoint saved, past the input it covered.
|
|
75
|
+
func resumeHash(checkpoint string, in io.Reader) (hash.Hash, int64, error) {
|
|
76
|
+
h := sha256.New()
|
|
77
|
+
if checkpoint == "" {
|
|
78
|
+
return h, 0, nil
|
|
79
|
+
}
|
|
80
|
+
var saved hashState
|
|
81
|
+
if err := json.Unmarshal([]byte(checkpoint), &saved); err != nil {
|
|
82
|
+
return nil, 0, fmt.Errorf("checkpoint: %w", err)
|
|
83
|
+
}
|
|
84
|
+
if saved.Offset < 0 {
|
|
85
|
+
return nil, 0, errors.New("checkpoint: negative offset")
|
|
86
|
+
}
|
|
87
|
+
if err := h.(encoding.BinaryUnmarshaler).UnmarshalBinary(saved.State); err != nil {
|
|
88
|
+
return nil, 0, fmt.Errorf("checkpoint: %w", err)
|
|
89
|
+
}
|
|
90
|
+
if _, err := io.CopyN(io.Discard, in, saved.Offset); err != nil {
|
|
91
|
+
return nil, 0, fmt.Errorf("checkpoint: %w", err)
|
|
92
|
+
}
|
|
93
|
+
return h, saved.Offset, nil
|
|
94
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
package app
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"context"
|
|
6
|
+
"crypto/sha256"
|
|
7
|
+
"encoding/hex"
|
|
8
|
+
"io"
|
|
9
|
+
"path/filepath"
|
|
10
|
+
"testing"
|
|
11
|
+
"time"
|
|
12
|
+
|
|
13
|
+
"github.com/synopackageland/syno-pkg-land-sdk/background"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
func TestHashWorkResumesFromItsCheckpoint(t *testing.T) {
|
|
17
|
+
head, tail := bytes.Repeat([]byte("h"), 1000), bytes.Repeat([]byte("t"), 3000)
|
|
18
|
+
h := sha256.New()
|
|
19
|
+
h.Write(head)
|
|
20
|
+
saved, err := saveHash(h, int64(len(head)))
|
|
21
|
+
if err != nil {
|
|
22
|
+
t.Fatal(err)
|
|
23
|
+
}
|
|
24
|
+
// The input's first bytes differ from what the checkpoint hashed: only a
|
|
25
|
+
// Work that skips them and continues the saved hash reaches this digest.
|
|
26
|
+
input := append(bytes.Repeat([]byte("x"), len(head)), tail...)
|
|
27
|
+
task := &background.Task{Job: background.Job{Checkpoint: saved}, Input: bytes.NewReader(input)}
|
|
28
|
+
got, err := HashWork(context.Background(), task)
|
|
29
|
+
want := sha256.Sum256(append(head, tail...))
|
|
30
|
+
if err != nil || got.SHA256 != hex.EncodeToString(want[:]) || got.Bytes != len(input) {
|
|
31
|
+
t.Fatalf("%+v %v", got, err)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
func TestHashWorkRefusesAForeignCheckpoint(t *testing.T) {
|
|
36
|
+
for _, saved := range []string{"{", `{"offset":-1,"state":""}`, `{"offset":3,"state":"AAAA"}`} {
|
|
37
|
+
task := &background.Task{Job: background.Job{Checkpoint: saved}, Input: bytes.NewReader([]byte("abc"))}
|
|
38
|
+
if _, err := HashWork(context.Background(), task); err == nil {
|
|
39
|
+
t.Errorf("%s: accepted", saved)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// stopAfter ends the worker's context once n bytes have been read, like a
|
|
45
|
+
// package stop in the middle of a job.
|
|
46
|
+
type stopAfter struct {
|
|
47
|
+
io.Reader
|
|
48
|
+
n int
|
|
49
|
+
stop context.CancelFunc
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
func (s *stopAfter) Read(p []byte) (int, error) {
|
|
53
|
+
n, err := s.Reader.Read(p)
|
|
54
|
+
if s.n -= n; s.n <= 0 {
|
|
55
|
+
s.stop()
|
|
56
|
+
}
|
|
57
|
+
return n, err
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
func TestHashWorkReportsProgressAndCheckpointsBeforeAStop(t *testing.T) {
|
|
61
|
+
store := background.NewStore(filepath.Join(t.TempDir(), "background"))
|
|
62
|
+
ctx := context.Background()
|
|
63
|
+
data := bytes.Repeat([]byte("0123456789abcdef"), 16<<10)
|
|
64
|
+
job, _, err := store.Submit(ctx, background.SubmitRequest{Key: "k", Name: "a.bin", Data: data, Submitter: background.Submitter{User: "alice", UID: "1024"}})
|
|
65
|
+
if err != nil {
|
|
66
|
+
t.Fatal(err)
|
|
67
|
+
}
|
|
68
|
+
worker, stop := context.WithCancel(ctx)
|
|
69
|
+
runner := &background.Runner{Store: store, Timeout: time.Minute, CancelPoll: 10 * time.Millisecond, Work: func(ctx context.Context, t *background.Task) (background.Result, error) {
|
|
70
|
+
t.Input = &stopAfter{Reader: t.Input, n: 3 * int(hashCheckpointEvery) / 2, stop: stop}
|
|
71
|
+
return HashWork(ctx, t)
|
|
72
|
+
}}
|
|
73
|
+
claimed, err := store.Claim(ctx)
|
|
74
|
+
if err != nil || claimed == nil {
|
|
75
|
+
t.Fatalf("%+v %v", claimed, err)
|
|
76
|
+
}
|
|
77
|
+
runner.Process(worker, claimed)
|
|
78
|
+
paused, err := store.Get(ctx, job.ID)
|
|
79
|
+
if err != nil || paused.State != background.StatePaused || paused.Checkpoint == "" || paused.Progress == nil ||
|
|
80
|
+
paused.Progress.Bytes < hashCheckpointEvery || paused.Progress.TotalBytes != int64(len(data)) {
|
|
81
|
+
t.Fatalf("%+v %+v %v", paused, paused.Progress, err)
|
|
82
|
+
}
|
|
83
|
+
if err := store.Recover(ctx); err != nil {
|
|
84
|
+
t.Fatal(err)
|
|
85
|
+
}
|
|
86
|
+
claimed, err = store.Claim(ctx)
|
|
87
|
+
if err != nil || claimed == nil || claimed.Checkpoint != paused.Checkpoint {
|
|
88
|
+
t.Fatalf("%+v %v", claimed, err)
|
|
89
|
+
}
|
|
90
|
+
runner.Work = HashWork
|
|
91
|
+
runner.Process(ctx, claimed)
|
|
92
|
+
if done, err := store.Get(ctx, job.ID); err != nil || done.State != background.StateSucceeded || done.Progress.Bytes != int64(len(data)) {
|
|
93
|
+
t.Fatalf("%+v %v", done, err)
|
|
94
|
+
}
|
|
95
|
+
}
|