@synopackageland/templates 0.9.8 → 0.10.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.
- package/README.md +27 -27
- package/package.json +2 -2
- package/templates/_shared/AGENTS.md +53 -15
- package/templates/native-go/backend/THIRD_PARTY_NOTICES.txt +1 -1
- package/templates/native-go/backend/cmd/app/main.go +15 -1
- package/templates/native-go/backend/cmd/app/main_test.go +33 -0
- package/templates/native-go/backend/go.mod +1 -1
- package/templates/native-go/backend/go.sum +2 -2
- package/templates/native-go/i18n/glossary.json +3 -1
- package/templates/native-go/i18n/messages/de.json +6 -0
- package/templates/native-go/i18n/messages/en.json +6 -0
- package/templates/native-go/i18n/messages/es.json +6 -0
- package/templates/native-go/i18n/messages/fr.json +6 -0
- package/templates/native-go/i18n/messages/it.json +6 -0
- package/templates/native-go/i18n/messages/ja.json +6 -0
- package/templates/native-go/i18n/messages/ko.json +6 -0
- package/templates/native-go/i18n/messages/zh-CN.json +6 -0
- package/templates/native-go/i18n/messages/zh-TW.json +6 -0
- package/templates/native-go/ui/app.js +2 -1
- package/templates/native-go-delegate/frontend/test/capability-panels.test.ts +3 -2
- package/templates/native-go-jobs/frontend/test/background-page.test.ts +4 -4
- package/templates/native-go-vue/README.md +19 -7
- package/templates/native-go-vue/frontend/package-lock.json +9 -9
- package/templates/native-go-vue/frontend/package.json +2 -2
- package/templates/native-go-vue/frontend/src/App.vue +31 -36
- package/templates/native-go-vue/frontend/test/apps.test.ts +65 -12
- package/templates/native-go-vue/frontend/test/context.ts +8 -0
- package/templates/native-go-vue/frontend/test/native-controls.test.ts +3 -4
- package/templates/native-go-vue/frontend/test/shell-i18n.test.ts +2 -2
- package/templates/native-node/README.md +153 -0
- package/templates/native-node/backend/app/package.json +4 -0
- package/templates/native-node/backend/app/permissions.json +5 -0
- package/templates/native-node/backend/package-lock.json +59 -0
- package/templates/native-node/backend/package.json +17 -0
- package/templates/native-node/backend/src/delegate.ts +5 -0
- package/templates/native-node/backend/src/jobs.ts +5 -0
- package/templates/native-node/backend/src/main.ts +4 -0
- package/templates/native-node/backend/src/params.ts +11 -0
- package/templates/native-node/backend/src/root-ops.ts +5 -0
- package/templates/native-node/backend/src/routes.ts +97 -0
- package/templates/native-node/backend/src/service.ts +22 -0
- package/templates/native-node/backend/test/routes.test.ts +75 -0
- package/templates/native-node/backend/test/support.ts +67 -0
- package/templates/native-node/backend/tsconfig.build.json +8 -0
- package/templates/native-node/backend/tsconfig.json +18 -0
- package/templates/native-node/dsm-package.yaml +80 -0
- package/templates/native-node/gitignore +4 -0
- package/templates/native-node-delegate/backend/src/delegate.ts +35 -0
- package/templates/native-node-delegate/backend/test/delegate.test.ts +51 -0
- package/templates/native-node-jobs/backend/src/jobs.ts +38 -0
- package/templates/native-node-jobs/backend/test/jobs.test.ts +73 -0
- package/templates/native-node-root-ops/backend/src/root-ops.ts +16 -0
- package/templates/native-node-root-ops/backend/test/root-ops.test.ts +23 -0
- package/templates/native-node-vue/README.md +56 -0
- package/templates/native-node-vue/gitignore +4 -0
- package/templates/native-python/README.md +150 -0
- package/templates/native-python/backend/app/delegate.py +5 -0
- package/templates/native-python/backend/app/jobs.py +5 -0
- package/templates/native-python/backend/app/main.py +5 -0
- package/templates/native-python/backend/app/params.py +10 -0
- package/templates/native-python/backend/app/permissions.json +5 -0
- package/templates/native-python/backend/app/root_ops.py +5 -0
- package/templates/native-python/backend/app/routes.py +107 -0
- package/templates/native-python/backend/tests/support.py +72 -0
- package/templates/native-python/backend/tests/test_routes.py +73 -0
- package/templates/native-python/dsm-package.yaml +77 -0
- package/templates/native-python/gitignore +2 -0
- package/templates/native-python-delegate/backend/app/delegate.py +42 -0
- package/templates/native-python-delegate/backend/tests/test_delegate.py +58 -0
- package/templates/native-python-jobs/backend/app/jobs.py +40 -0
- package/templates/native-python-jobs/backend/tests/test_jobs.py +72 -0
- package/templates/native-python-root-ops/backend/app/root_ops.py +18 -0
- package/templates/native-python-root-ops/backend/tests/test_root_ops.py +34 -0
- package/templates/native-python-vue/README.md +56 -0
- package/templates/native-python-vue/gitignore +3 -0
- package/templates/native-rust/README.md +74 -296
- package/templates/native-rust/backend/Cargo.lock +6 -6
- package/templates/native-rust/backend/Cargo.toml +2 -10
- package/templates/native-rust/backend/src/delegate.rs +6 -0
- package/templates/native-rust/backend/src/jobs.rs +6 -0
- package/templates/native-rust/backend/src/main.rs +12 -0
- package/templates/native-rust/backend/src/root_ops.rs +6 -0
- package/templates/native-rust/backend/src/routes.rs +352 -0
- package/templates/native-rust/backend/src/test_support.rs +91 -0
- package/templates/native-rust/dsm-package.yaml +10 -21
- package/templates/native-rust-delegate/backend/src/delegate.rs +171 -0
- package/templates/native-rust-jobs/backend/src/jobs.rs +240 -0
- package/templates/native-rust-root-ops/backend/src/root_ops.rs +83 -0
- package/templates/native-rust-vue/README.md +35 -34
- package/templates/native-rust/backend/THIRD_PARTY_NOTICES.txt +0 -207
- package/templates/native-rust/backend/src/app/background/mod.rs +0 -424
- package/templates/native-rust/backend/src/app/background/owner.rs +0 -77
- package/templates/native-rust/backend/src/app/background/owner_tests.rs +0 -174
- package/templates/native-rust/backend/src/app/background/schedule.rs +0 -126
- package/templates/native-rust/backend/src/app/background/schedule_tests.rs +0 -108
- package/templates/native-rust/backend/src/app/background/tests.rs +0 -505
- package/templates/native-rust/backend/src/app/background/work.rs +0 -78
- package/templates/native-rust/backend/src/app/background/work_tests.rs +0 -123
- package/templates/native-rust/backend/src/app/background_routes.rs +0 -22
- package/templates/native-rust/backend/src/app/delegate_config.rs +0 -188
- package/templates/native-rust/backend/src/app/delegate_routes.rs +0 -22
- package/templates/native-rust/backend/src/app/identity.rs +0 -56
- package/templates/native-rust/backend/src/app/migrations.rs +0 -79
- package/templates/native-rust/backend/src/app/mod.rs +0 -30
- package/templates/native-rust/backend/src/app/portal.rs +0 -317
- package/templates/native-rust/backend/src/app/root_routes.rs +0 -23
- package/templates/native-rust/backend/src/app/routes.rs +0 -315
- package/templates/native-rust/backend/src/app/session_files.rs +0 -19
- package/templates/native-rust/backend/src/app/shared_folder.rs +0 -383
- package/templates/native-rust/backend/src/app/test_support.rs +0 -202
- package/templates/native-rust/backend/src/app/uninstall.rs +0 -38
- package/templates/native-rust/backend/src/bin/api.rs +0 -72
- package/templates/native-rust/backend/src/bin/background-worker.rs +0 -37
- package/templates/native-rust/backend/src/build_env.rs +0 -155
- package/templates/native-rust/backend/src/lib.rs +0 -6
- package/templates/native-rust/backend/src/permissions.rs +0 -54
- package/templates/native-rust/backend/src/process.rs +0 -91
- package/templates/native-rust/i18n/glossary.json +0 -663
- package/templates/native-rust/i18n/messages/de.json +0 -30
- package/templates/native-rust/i18n/messages/en.json +0 -30
- package/templates/native-rust/i18n/messages/es.json +0 -30
- package/templates/native-rust/i18n/messages/fr.json +0 -30
- package/templates/native-rust/i18n/messages/it.json +0 -30
- package/templates/native-rust/i18n/messages/ja.json +0 -30
- package/templates/native-rust/i18n/messages/ko.json +0 -30
- package/templates/native-rust/i18n/messages/zh-CN.json +0 -30
- package/templates/native-rust/i18n/messages/zh-TW.json +0 -30
- package/templates/native-rust/ui/app.css +0 -45
- package/templates/native-rust/ui/app.js +0 -111
- package/templates/native-rust-delegate/backend/src/app/delegate_routes.rs +0 -92
- package/templates/native-rust-delegate/backend/src/bin/background-worker.rs +0 -52
- package/templates/native-rust-jobs/backend/src/app/background_routes.rs +0 -109
- package/templates/native-rust-root-ops/backend/src/app/root_routes.rs +0 -475
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
use std::sync::Arc;
|
|
2
|
-
|
|
3
|
-
use syno_pkg_land_sdk::api::Route;
|
|
4
|
-
|
|
5
|
-
#[cfg(test)]
|
|
6
|
-
use super::test_support::{Sample, sample};
|
|
7
|
-
use super::{BackgroundApi, Config, system_uid_lookup};
|
|
8
|
-
|
|
9
|
-
impl Config {
|
|
10
|
-
/// Serves the owner-scoped background job API. Jobs run in the
|
|
11
|
-
/// package-user worker unit (conf/systemd), never as root.
|
|
12
|
-
pub(super) fn background_routes(self: &Arc<Self>) -> Vec<Route> {
|
|
13
|
-
let uid_lookup = self.uid_lookup.clone().unwrap_or_else(system_uid_lookup);
|
|
14
|
-
BackgroundApi::production(&self.data_dir, uid_lookup).routes()
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/// Valid params for every background route. The fixture has no DSM cookie, so
|
|
19
|
-
/// import is refused before reading a file, and its fresh data directory has
|
|
20
|
-
/// no jobs or schedules.
|
|
21
|
-
#[cfg(test)]
|
|
22
|
-
pub(super) fn samples() -> Vec<Sample> {
|
|
23
|
-
let id = r#"{"id":"missing"}"#;
|
|
24
|
-
vec![
|
|
25
|
-
sample(
|
|
26
|
-
"bg.jobs.purge",
|
|
27
|
-
r#"{"owner":{"user":"missing","uid":"9999"}}"#,
|
|
28
|
-
"",
|
|
29
|
-
),
|
|
30
|
-
sample("bg.schedules.list", "{}", ""),
|
|
31
|
-
sample(
|
|
32
|
-
"bg.schedules.create",
|
|
33
|
-
r#"{"key":"schedule","jobId":"missing","intervalSeconds":3600,"consent":true}"#,
|
|
34
|
-
"not_found",
|
|
35
|
-
),
|
|
36
|
-
sample("bg.schedules.disable", id, "not_found"),
|
|
37
|
-
sample("bg.schedules.delete", id, "not_found"),
|
|
38
|
-
sample(
|
|
39
|
-
"bg.schedules.enable",
|
|
40
|
-
r#"{"id":"missing","consent":true}"#,
|
|
41
|
-
"not_found",
|
|
42
|
-
),
|
|
43
|
-
sample("bg.status", "{}", ""),
|
|
44
|
-
sample("bg.browse", "{}", "session_required"),
|
|
45
|
-
sample(
|
|
46
|
-
"bg.jobs.submit",
|
|
47
|
-
r#"{"key":"k1","folder":"/share","relative":"a.bin"}"#,
|
|
48
|
-
"session_required",
|
|
49
|
-
),
|
|
50
|
-
sample("bg.jobs.list", "{}", ""),
|
|
51
|
-
sample("bg.jobs.get", id, "not_found"),
|
|
52
|
-
sample("bg.jobs.cancel", id, "not_found"),
|
|
53
|
-
sample("bg.jobs.pause", id, "not_found"),
|
|
54
|
-
sample("bg.jobs.resume", id, "not_found"),
|
|
55
|
-
sample("bg.jobs.result", id, "not_found"),
|
|
56
|
-
]
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
#[cfg(test)]
|
|
60
|
-
mod tests {
|
|
61
|
-
use std::time::{Duration, Instant};
|
|
62
|
-
|
|
63
|
-
use syno_pkg_land_sdk::background::{REQUEST_LOCK_TIMEOUT, Store};
|
|
64
|
-
use syno_pkg_land_sdk::ctx::Context;
|
|
65
|
-
|
|
66
|
-
use super::super::test_support::*;
|
|
67
|
-
|
|
68
|
-
/// Holds the spool's journal lock, as a busy worker does.
|
|
69
|
-
struct Flock(std::fs::File);
|
|
70
|
-
|
|
71
|
-
impl Flock {
|
|
72
|
-
fn exclusive(path: &std::path::Path) -> Flock {
|
|
73
|
-
use std::os::fd::AsRawFd;
|
|
74
|
-
let file = std::fs::OpenOptions::new()
|
|
75
|
-
.read(true)
|
|
76
|
-
.write(true)
|
|
77
|
-
.open(path)
|
|
78
|
-
.unwrap();
|
|
79
|
-
// SAFETY: the descriptor is open for the file's lifetime.
|
|
80
|
-
let rc = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
|
|
81
|
-
assert_eq!(rc, 0, "{}", std::io::Error::last_os_error());
|
|
82
|
-
Flock(file)
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
impl Drop for Flock {
|
|
87
|
-
fn drop(&mut self) {
|
|
88
|
-
use std::os::fd::AsRawFd;
|
|
89
|
-
// SAFETY: the descriptor is open for the file's lifetime.
|
|
90
|
-
unsafe { libc::flock(self.0.as_raw_fd(), libc::LOCK_UN) };
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
#[test]
|
|
95
|
-
fn a_busy_spool_is_reported_within_the_request_budget() {
|
|
96
|
-
let fixture = Fixture::new();
|
|
97
|
-
let dir = fixture.cfg.data_dir.join("background");
|
|
98
|
-
// The first read creates the spool and its lock.
|
|
99
|
-
Store::new_request(&dir)
|
|
100
|
-
.list(&Context::background())
|
|
101
|
-
.unwrap();
|
|
102
|
-
let _held = Flock::exclusive(&dir.join("lock"));
|
|
103
|
-
|
|
104
|
-
let start = Instant::now();
|
|
105
|
-
let (code, out) = call(&fixture.cfg, &alice(), "bg.jobs.list", "{}");
|
|
106
|
-
assert_eq!(code, "jobs_busy", "{out}");
|
|
107
|
-
assert!(start.elapsed() < REQUEST_LOCK_TIMEOUT + Duration::from_secs(1));
|
|
108
|
-
}
|
|
109
|
-
}
|
|
@@ -1,475 +0,0 @@
|
|
|
1
|
-
use std::io::Read;
|
|
2
|
-
use std::os::unix::fs::DirBuilderExt;
|
|
3
|
-
use std::sync::Arc;
|
|
4
|
-
use std::time::Duration;
|
|
5
|
-
|
|
6
|
-
use serde::Deserialize;
|
|
7
|
-
use serde_json::Value;
|
|
8
|
-
use syno_pkg_land_sdk::BoxError;
|
|
9
|
-
use syno_pkg_land_sdk::api::{self, Caller, Route};
|
|
10
|
-
use syno_pkg_land_sdk::ctx::Context;
|
|
11
|
-
use syno_pkg_land_sdk::delegate::{
|
|
12
|
-
self, Auth, CODE_EXECUTOR_UNAVAILABLE, CODE_POLICY_DENIED, Client, ExecutorClient,
|
|
13
|
-
ExecutorStatus,
|
|
14
|
-
};
|
|
15
|
-
use syno_pkg_land_sdk::http::Request;
|
|
16
|
-
use syno_pkg_land_sdk::jsonx::Empty;
|
|
17
|
-
|
|
18
|
-
use super::Config;
|
|
19
|
-
use super::routes::{decode_params, write_file_atomic};
|
|
20
|
-
#[cfg(test)]
|
|
21
|
-
use super::test_support::{Sample, sample};
|
|
22
|
-
|
|
23
|
-
impl Config {
|
|
24
|
-
pub(super) fn root_routes(self: &Arc<Self>) -> Vec<Route> {
|
|
25
|
-
vec![
|
|
26
|
-
self.handle("executor.control", Config::executor_control),
|
|
27
|
-
self.handle("executor.status", Config::executor_status),
|
|
28
|
-
self.handle_request("root.run", Config::executor_root_run),
|
|
29
|
-
]
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/// API permissions default to administrators for unlisted methods. Check
|
|
33
|
-
/// here too: control mutates the fixed package-owned control file before
|
|
34
|
-
/// DSM runs it.
|
|
35
|
-
fn executor_control(
|
|
36
|
-
&self,
|
|
37
|
-
_: &Context,
|
|
38
|
-
caller: &Caller,
|
|
39
|
-
raw: &[u8],
|
|
40
|
-
) -> Result<Value, BoxError> {
|
|
41
|
-
api::require_admin(caller)?;
|
|
42
|
-
#[derive(Default, Deserialize)]
|
|
43
|
-
#[serde(deny_unknown_fields)]
|
|
44
|
-
struct Params {
|
|
45
|
-
#[serde(default)]
|
|
46
|
-
action: String,
|
|
47
|
-
}
|
|
48
|
-
let params: Params = decode_params(raw)?;
|
|
49
|
-
if !["start", "stop", "purge"].contains(¶ms.action.as_str()) {
|
|
50
|
-
return Err(api::Error::bad_request("executor_control_invalid").into());
|
|
51
|
-
}
|
|
52
|
-
let dir = self.data_dir.join("executor");
|
|
53
|
-
std::fs::DirBuilder::new()
|
|
54
|
-
.recursive(true)
|
|
55
|
-
.mode(0o700)
|
|
56
|
-
.create(&dir)?;
|
|
57
|
-
let info = std::fs::symlink_metadata(&dir)?;
|
|
58
|
-
if !info.is_dir() || info.file_type().is_symlink() {
|
|
59
|
-
return Err("executor_control_directory_invalid".into());
|
|
60
|
-
}
|
|
61
|
-
write_file_atomic(&dir.join("control"), params.action.as_bytes())?;
|
|
62
|
-
Ok(serde_json::json!({"written": true}))
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/// The executor closes its socket while paused: the package is stopped,
|
|
66
|
-
/// or an upgrade changed the pins and awaits administrator approval.
|
|
67
|
-
fn executor_status(&self, ctx: &Context, _: &Caller, _: &[u8]) -> Result<Value, BoxError> {
|
|
68
|
-
match delegate::call::<_, Value>(&*self.executor(), ctx, "status", &Auth::new(), &Empty {})
|
|
69
|
-
{
|
|
70
|
-
Err(err)
|
|
71
|
-
if api::find_error::<api::Error>(&*err)
|
|
72
|
-
.is_some_and(|refusal| refusal.code == CODE_EXECUTOR_UNAVAILABLE) =>
|
|
73
|
-
{
|
|
74
|
-
let next = if self.start_requested() {
|
|
75
|
-
"The package is running: an administrator approves background access again in Administrator setup; the executor log names the cause."
|
|
76
|
-
} else {
|
|
77
|
-
"Start the package."
|
|
78
|
-
};
|
|
79
|
-
Ok(serde_json::json!({
|
|
80
|
-
"ready": false, "state": "paused",
|
|
81
|
-
"reason": "executor_not_listening", "next": next,
|
|
82
|
-
}))
|
|
83
|
-
}
|
|
84
|
-
status => status,
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/// Package start and stop write the lifecycle mailbox; its last request
|
|
89
|
-
/// tells a stopped package from a running one whose executor stays paused.
|
|
90
|
-
fn start_requested(&self) -> bool {
|
|
91
|
-
let Ok(mut file) = std::fs::File::open(self.data_dir.join("executor").join("control"))
|
|
92
|
-
else {
|
|
93
|
-
return false;
|
|
94
|
-
};
|
|
95
|
-
let mut buf = [0u8; 16];
|
|
96
|
-
let n = file.read(&mut buf).unwrap_or(0);
|
|
97
|
-
String::from_utf8_lossy(&buf[..n]).trim() == "start"
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
fn executor_root_run(
|
|
101
|
-
&self,
|
|
102
|
-
ctx: &Context,
|
|
103
|
-
caller: &Caller,
|
|
104
|
-
req: &Request,
|
|
105
|
-
raw: &[u8],
|
|
106
|
-
) -> Result<Value, BoxError> {
|
|
107
|
-
api::require_admin(caller)?;
|
|
108
|
-
#[derive(Default, Deserialize)]
|
|
109
|
-
#[serde(deny_unknown_fields)]
|
|
110
|
-
struct Params {
|
|
111
|
-
#[serde(default)]
|
|
112
|
-
name: String,
|
|
113
|
-
}
|
|
114
|
-
let params: Params = decode_params(raw)?;
|
|
115
|
-
// The executor independently verifies both session values and the
|
|
116
|
-
// administrator identity.
|
|
117
|
-
let auth = api::dsm_session(req)?;
|
|
118
|
-
self.executor_holds_this_build(ctx)?;
|
|
119
|
-
delegate::call(
|
|
120
|
-
&*self.executor(),
|
|
121
|
-
ctx,
|
|
122
|
-
"root.run",
|
|
123
|
-
&auth,
|
|
124
|
-
&serde_json::json!({"name": params.name}),
|
|
125
|
-
)
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/// Refuses, as delegate does, an executor pinned to another build's
|
|
129
|
-
/// policy or executor: its approved scripts are not this build's.
|
|
130
|
-
fn executor_holds_this_build(&self, ctx: &Context) -> Result<(), BoxError> {
|
|
131
|
-
let pins: ExecutorStatus =
|
|
132
|
-
delegate::call(&*self.executor(), ctx, "status", &Auth::new(), &Empty {})?;
|
|
133
|
-
if (!self.executor_digest.is_empty() && pins.executor_digest != self.executor_digest)
|
|
134
|
-
|| (!self.policy_digest.is_empty() && pins.policy_digest != self.policy_digest)
|
|
135
|
-
{
|
|
136
|
-
return Err(api::Error::new(
|
|
137
|
-
CODE_POLICY_DENIED,
|
|
138
|
-
"the executor holds another build's approved policy; an administrator approves this build in Administrator setup",
|
|
139
|
-
)
|
|
140
|
-
.into());
|
|
141
|
-
}
|
|
142
|
-
Ok(())
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/// Keeps the executor's protocol v1 refusal codes (POLICY_DENIED,
|
|
146
|
-
/// EXECUTOR_UNAVAILABLE, ...) as the API error code.
|
|
147
|
-
fn executor(&self) -> Arc<dyn ExecutorClient> {
|
|
148
|
-
self.executor.clone().unwrap_or_else(|| {
|
|
149
|
-
Arc::new(Client {
|
|
150
|
-
timeout: Duration::from_secs(30),
|
|
151
|
-
..Client::local(&self.package_id)
|
|
152
|
-
})
|
|
153
|
-
})
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/// Valid params for the root routes. root.run needs a DSM session the
|
|
158
|
-
/// fixture does not have.
|
|
159
|
-
#[cfg(test)]
|
|
160
|
-
pub(super) fn samples() -> Vec<Sample> {
|
|
161
|
-
vec![
|
|
162
|
-
sample("executor.control", r#"{"action":"stop"}"#, ""),
|
|
163
|
-
sample("executor.status", "{}", ""),
|
|
164
|
-
sample("root.run", r#"{"name":"whoami"}"#, "session_required"),
|
|
165
|
-
]
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
#[cfg(test)]
|
|
169
|
-
mod tests {
|
|
170
|
-
use std::io::{BufRead, BufReader, Write};
|
|
171
|
-
use std::os::unix::net::UnixListener;
|
|
172
|
-
use std::path::PathBuf;
|
|
173
|
-
use std::sync::Mutex;
|
|
174
|
-
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
|
175
|
-
|
|
176
|
-
use super::super::test_support::*;
|
|
177
|
-
use super::*;
|
|
178
|
-
use crate::permissions::permissions;
|
|
179
|
-
|
|
180
|
-
/// Unix socket paths are limited to about 100 bytes, so the executor's
|
|
181
|
-
/// fake lives in a short directory under /tmp.
|
|
182
|
-
struct SocketBase(PathBuf);
|
|
183
|
-
|
|
184
|
-
impl SocketBase {
|
|
185
|
-
fn new() -> Self {
|
|
186
|
-
static NEXT: AtomicU64 = AtomicU64::new(0);
|
|
187
|
-
let base = PathBuf::from(format!(
|
|
188
|
-
"/tmp/exec-{}-{}",
|
|
189
|
-
std::process::id(),
|
|
190
|
-
NEXT.fetch_add(1, Ordering::SeqCst)
|
|
191
|
-
));
|
|
192
|
-
let _ = std::fs::remove_dir_all(&base);
|
|
193
|
-
std::fs::create_dir_all(&base).unwrap();
|
|
194
|
-
SocketBase(base)
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
fn socket(&self, cfg: &Config) -> PathBuf {
|
|
198
|
-
let dir = self.0.join(&cfg.package_id);
|
|
199
|
-
std::fs::create_dir_all(&dir).unwrap();
|
|
200
|
-
dir.join("executor.sock")
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/// Points cfg at an executor under this base.
|
|
204
|
-
fn attach(&self, cfg: &mut Config) {
|
|
205
|
-
cfg.executor = Some(Arc::new(Client {
|
|
206
|
-
socket: self.socket(cfg).to_string_lossy().into_owned(),
|
|
207
|
-
timeout: Duration::from_secs(5),
|
|
208
|
-
}));
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
impl Drop for SocketBase {
|
|
213
|
-
fn drop(&mut self) {
|
|
214
|
-
let _ = std::fs::remove_dir_all(&self.0);
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/// Answers each v1 request with replies[op] and records the ops.
|
|
219
|
-
struct FakeExecutor {
|
|
220
|
-
ops: Arc<Mutex<Vec<String>>>,
|
|
221
|
-
stop: Arc<AtomicBool>,
|
|
222
|
-
socket: PathBuf,
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
impl FakeExecutor {
|
|
226
|
-
fn start(base: &SocketBase, cfg: &mut Config, replies: &[(&str, String)]) -> Self {
|
|
227
|
-
base.attach(cfg);
|
|
228
|
-
let socket = base.socket(cfg);
|
|
229
|
-
let listener = UnixListener::bind(&socket).unwrap();
|
|
230
|
-
let replies: Vec<(String, String)> = replies
|
|
231
|
-
.iter()
|
|
232
|
-
.map(|(op, reply)| (op.to_string(), reply.clone()))
|
|
233
|
-
.collect();
|
|
234
|
-
let ops = Arc::new(Mutex::new(Vec::new()));
|
|
235
|
-
let stop = Arc::new(AtomicBool::new(false));
|
|
236
|
-
let (seen, done) = (ops.clone(), stop.clone());
|
|
237
|
-
std::thread::spawn(move || {
|
|
238
|
-
for conn in listener.incoming() {
|
|
239
|
-
if done.load(Ordering::SeqCst) {
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
let Ok(mut conn) = conn else { return };
|
|
243
|
-
let mut line = String::new();
|
|
244
|
-
let _ = BufReader::new(&conn).read_line(&mut line);
|
|
245
|
-
let request: Value = serde_json::from_str(&line).unwrap_or_default();
|
|
246
|
-
let op = request["op"].as_str().unwrap_or("").to_string();
|
|
247
|
-
let reply = replies
|
|
248
|
-
.iter()
|
|
249
|
-
.find(|(name, _)| *name == op)
|
|
250
|
-
.map(|(_, reply)| reply.clone())
|
|
251
|
-
.unwrap_or_default();
|
|
252
|
-
seen.lock().unwrap().push(op);
|
|
253
|
-
let _ = writeln!(conn, "{reply}");
|
|
254
|
-
}
|
|
255
|
-
});
|
|
256
|
-
FakeExecutor { ops, stop, socket }
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
fn received(&self) -> Vec<String> {
|
|
260
|
-
self.ops.lock().unwrap().clone()
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
impl Drop for FakeExecutor {
|
|
265
|
-
fn drop(&mut self) {
|
|
266
|
-
self.stop.store(true, Ordering::SeqCst);
|
|
267
|
-
let _ = std::os::unix::net::UnixStream::connect(&self.socket);
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
fn data(out: &Value) -> &Value {
|
|
272
|
-
&out["data"]
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
#[test]
|
|
276
|
-
fn executor_control_accepts_only_fixed_actions() {
|
|
277
|
-
let fixture = Fixture::new();
|
|
278
|
-
let cfg = &fixture.cfg;
|
|
279
|
-
for action in ["start", "stop", "purge"] {
|
|
280
|
-
let (code, out) = call(
|
|
281
|
-
cfg,
|
|
282
|
-
&alice(),
|
|
283
|
-
"executor.control",
|
|
284
|
-
&format!(r#"{{"action":"{action}"}}"#),
|
|
285
|
-
);
|
|
286
|
-
assert_eq!(code, "", "{action}: {out}");
|
|
287
|
-
let written = std::fs::read_to_string(cfg.data_dir.join("executor/control")).unwrap();
|
|
288
|
-
assert_eq!(written, action);
|
|
289
|
-
}
|
|
290
|
-
for raw in [
|
|
291
|
-
r#"{"action":"start;id"}"#,
|
|
292
|
-
r#"{"action":"start\nstop"}"#,
|
|
293
|
-
r#"{"action":""}"#,
|
|
294
|
-
r#"{"action":"stop","path":"/etc/passwd"}"#,
|
|
295
|
-
] {
|
|
296
|
-
assert_eq!(
|
|
297
|
-
call(cfg, &alice(), "executor.control", raw).0,
|
|
298
|
-
"bad_request",
|
|
299
|
-
"{raw}"
|
|
300
|
-
);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
#[test]
|
|
305
|
-
fn executor_control_rejects_a_symlinked_directory() {
|
|
306
|
-
let fixture = Fixture::new();
|
|
307
|
-
let outside = fixture.scratch();
|
|
308
|
-
std::os::unix::fs::symlink(&outside, fixture.cfg.data_dir.join("executor")).unwrap();
|
|
309
|
-
let (code, _) = call(
|
|
310
|
-
&fixture.cfg,
|
|
311
|
-
&alice(),
|
|
312
|
-
"executor.control",
|
|
313
|
-
r#"{"action":"stop"}"#,
|
|
314
|
-
);
|
|
315
|
-
assert_ne!(code, "", "accepted symlink");
|
|
316
|
-
assert!(
|
|
317
|
-
!outside.join("control").exists(),
|
|
318
|
-
"wrote outside data directory"
|
|
319
|
-
);
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
#[test]
|
|
323
|
-
fn root_run_requires_the_current_dsm_cookie() {
|
|
324
|
-
let fixture = Fixture::new();
|
|
325
|
-
let (code, _) = call(&fixture.cfg, &alice(), "root.run", r#"{"name":"whoami"}"#);
|
|
326
|
-
assert_ne!(code, "", "root.run accepted an absent DSM cookie");
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
/// The executor closes its listener while paused (package stopped, or pins
|
|
330
|
-
/// awaiting administrator approval). Status then answers, not "internal".
|
|
331
|
-
#[test]
|
|
332
|
-
fn executor_status_reports_a_paused_executor() {
|
|
333
|
-
let mut fixture = Fixture::new();
|
|
334
|
-
let base = SocketBase::new();
|
|
335
|
-
base.attach(&mut fixture.cfg);
|
|
336
|
-
let check = |name: &str| {
|
|
337
|
-
let (code, out) = call(&fixture.cfg, &alice(), "executor.status", "{}");
|
|
338
|
-
let d = data(&out);
|
|
339
|
-
assert_eq!(code, "", "{name}: {out}");
|
|
340
|
-
assert_eq!(d["ready"], false, "{name}: {out}");
|
|
341
|
-
assert_eq!(d["state"], "paused", "{name}: {out}");
|
|
342
|
-
assert_eq!(d["reason"], "executor_not_listening", "{name}: {out}");
|
|
343
|
-
assert!(
|
|
344
|
-
!d["next"].as_str().unwrap_or("").is_empty(),
|
|
345
|
-
"{name}: {out}"
|
|
346
|
-
);
|
|
347
|
-
};
|
|
348
|
-
check("no socket");
|
|
349
|
-
let (code, _) = call(&fixture.cfg, &bob(), "executor.status", "{}");
|
|
350
|
-
assert_eq!(
|
|
351
|
-
code, "",
|
|
352
|
-
"an authenticated user cannot read a paused status"
|
|
353
|
-
);
|
|
354
|
-
drop(UnixListener::bind(base.socket(&fixture.cfg)).unwrap());
|
|
355
|
-
check("closed listener");
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
/// next follows the last lifecycle request: a running package needs
|
|
359
|
-
/// approval again, not a start.
|
|
360
|
-
#[test]
|
|
361
|
-
fn executor_status_next_follows_the_lifecycle_request() {
|
|
362
|
-
let mut fixture = Fixture::new();
|
|
363
|
-
let base = SocketBase::new();
|
|
364
|
-
base.attach(&mut fixture.cfg);
|
|
365
|
-
let next = || {
|
|
366
|
-
let (_, out) = call(&fixture.cfg, &alice(), "executor.status", "{}");
|
|
367
|
-
data(&out)["next"].as_str().unwrap_or("").to_string()
|
|
368
|
-
};
|
|
369
|
-
assert!(
|
|
370
|
-
next().starts_with("Start the package."),
|
|
371
|
-
"no request: {}",
|
|
372
|
-
next()
|
|
373
|
-
);
|
|
374
|
-
let mailbox = fixture.cfg.data_dir.join("executor");
|
|
375
|
-
std::fs::create_dir_all(&mailbox).unwrap();
|
|
376
|
-
for (request, start) in [("stop\n", true), ("start\n", false), ("start", false)] {
|
|
377
|
-
std::fs::write(mailbox.join("control"), request).unwrap();
|
|
378
|
-
let got = next();
|
|
379
|
-
assert_eq!(
|
|
380
|
-
got.starts_with("Start the package"),
|
|
381
|
-
start,
|
|
382
|
-
"{request:?}: {got}"
|
|
383
|
-
);
|
|
384
|
-
assert!(!got.is_empty());
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
const READY: &str = r#"{"ok":true,"result":{"executor_digest":"e1","policy_digest":"p1","ready":true,"disabled":false,"version":"1"}}"#;
|
|
389
|
-
const WHOAMI: &str = r#"{"ok":true,"result":{"output":"0\n"}}"#;
|
|
390
|
-
|
|
391
|
-
fn pinned() -> Fixture {
|
|
392
|
-
let mut fixture = Fixture::new();
|
|
393
|
-
fixture.cfg.executor_digest = "e1".into();
|
|
394
|
-
fixture.cfg.policy_digest = "p1".into();
|
|
395
|
-
fixture
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
/// root.run as an administrator with a current DSM session.
|
|
399
|
-
fn root_run(cfg: &Config, name: &str) -> (String, Value) {
|
|
400
|
-
let mut req = api_request("root.run", &format!(r#"{{"name":"{name}"}}"#));
|
|
401
|
-
req.header.set("Cookie", "id=session");
|
|
402
|
-
req.header.set("X-SYNO-TOKEN", "token");
|
|
403
|
-
serve(cfg, &alice(), req, permissions().unwrap())
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
#[test]
|
|
407
|
-
fn root_run_runs_when_the_executor_holds_this_builds_pins() {
|
|
408
|
-
let mut fixture = pinned();
|
|
409
|
-
let base = SocketBase::new();
|
|
410
|
-
let _executor = FakeExecutor::start(
|
|
411
|
-
&base,
|
|
412
|
-
&mut fixture.cfg,
|
|
413
|
-
&[("status", READY.into()), ("root.run", WHOAMI.into())],
|
|
414
|
-
);
|
|
415
|
-
let (code, out) = root_run(&fixture.cfg, "whoami");
|
|
416
|
-
assert_eq!(code, "", "{out}");
|
|
417
|
-
assert_eq!(data(&out)["output"], "0\n");
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
/// Like delegate: an executor pinned to another build's policy or
|
|
421
|
-
/// executor refuses the operation, it does not run it.
|
|
422
|
-
#[test]
|
|
423
|
-
fn root_run_refuses_an_executor_pinned_to_another_build() {
|
|
424
|
-
for (name, status) in [
|
|
425
|
-
("policy", READY.replacen("\"p1\"", "\"p2\"", 1)),
|
|
426
|
-
("executor", READY.replacen("\"e1\"", "\"e2\"", 1)),
|
|
427
|
-
] {
|
|
428
|
-
let mut fixture = pinned();
|
|
429
|
-
let base = SocketBase::new();
|
|
430
|
-
let executor = FakeExecutor::start(
|
|
431
|
-
&base,
|
|
432
|
-
&mut fixture.cfg,
|
|
433
|
-
&[("status", status), ("root.run", WHOAMI.into())],
|
|
434
|
-
);
|
|
435
|
-
let (code, out) = root_run(&fixture.cfg, "whoami");
|
|
436
|
-
assert_eq!(code, "POLICY_DENIED", "{name} digest mismatch: {out}");
|
|
437
|
-
assert!(
|
|
438
|
-
!executor.received().contains(&"root.run".to_string()),
|
|
439
|
-
"{name} digest mismatch: root.run reached the executor"
|
|
440
|
-
);
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
/// Refusals keep the protocol v1 code (spec/protocol/v1/errors.json),
|
|
445
|
-
/// never "internal".
|
|
446
|
-
#[test]
|
|
447
|
-
fn root_run_reports_the_executors_refusal_code() {
|
|
448
|
-
let mut fixture = pinned();
|
|
449
|
-
let base = SocketBase::new();
|
|
450
|
-
base.attach(&mut fixture.cfg);
|
|
451
|
-
let (code, _) = root_run(&fixture.cfg, "whoami");
|
|
452
|
-
assert_eq!(
|
|
453
|
-
code, "EXECUTOR_UNAVAILABLE",
|
|
454
|
-
"paused or awaiting approval (no socket)"
|
|
455
|
-
);
|
|
456
|
-
for refusal in [
|
|
457
|
-
"POLICY_DENIED",
|
|
458
|
-
"INVALID_REQUEST",
|
|
459
|
-
"EXECUTOR_PAUSED",
|
|
460
|
-
"ROOT_FAILED",
|
|
461
|
-
"ADMIN_REQUIRED",
|
|
462
|
-
] {
|
|
463
|
-
let mut fixture = pinned();
|
|
464
|
-
let base = SocketBase::new();
|
|
465
|
-
let reply = format!(r#"{{"ok":false,"code":"{refusal}","message":"refused"}}"#);
|
|
466
|
-
let _executor = FakeExecutor::start(
|
|
467
|
-
&base,
|
|
468
|
-
&mut fixture.cfg,
|
|
469
|
-
&[("status", READY.into()), ("root.run", reply)],
|
|
470
|
-
);
|
|
471
|
-
let (code, out) = root_run(&fixture.cfg, "whoami");
|
|
472
|
-
assert_eq!(code, refusal, "{out}");
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
}
|