Skip to content

Commit f7c43a5

Browse files
authored
Merge pull request #206 from keszybz/system
Allow "set!var = program" at top level that makes var = eval(system(program))
2 parents 0fb828f + 0eda100 commit f7c43a5

File tree

7 files changed

+163
-25
lines changed

7 files changed

+163
-25
lines changed

man/index.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ zram-generator.conf(5) zram-generator.conf.5.ronn
33

44
modprobe(8) https://man7.org/linux/man-pages/man8/modprobe.8.html
55
proc(5) https://man7.org/linux/man-pages/man5/proc.5.html
6+
system(3) https://man7.org/linux/man-pages/man3/system.3.html
67

78
systemd-detect-virt(1) https://freedesktop.org/software/systemd/man/systemd-detect-virt.html
89
systemd.generator(7) https://freedesktop.org/software/systemd/man/systemd.generator.html

man/zram-generator.conf.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ This option thus has higher priority than the configuration files.
4242

4343
## OPTIONS
4444

45-
Each device is configured independently in its `[zramN]` section, where N is a nonnegative integer. Other sections are ignored.
45+
Each device is configured independently in its `[zramN]` section, where N is a nonnegative integer. The global section may contain [DIRECTIVES]. Other sections are ignored.
4646

4747
Devices with the final size of *0* will be discarded.
4848

@@ -57,6 +57,7 @@ Devices with the final size of *0* will be discarded.
5757
* `zram-size`=
5858

5959
Sets the size of the zram device as a function of *MemTotal*, available as the `ram` variable.
60+
Additional variables may be provided by [DIRECTIVES].
6061

6162
Arithmetic operators (^%/\*-+), e, π, SI suffixes, log(), int(), ceil(), floor(), round(), abs(), min(), max(), and trigonometric functions are supported.
6263

@@ -66,7 +67,7 @@ Devices with the final size of *0* will be discarded.
6667

6768
Sets the maximum resident memory limit of the zram device (or *0* for no limit) as a function of *MemTotal*, available as the `ram` variable.
6869

69-
Same format as *zram-size*. Defaults to *0*.
70+
Same format as `zram-size`. Defaults to *0*.
7071

7172
* `compression-algorithm`=
7273

@@ -117,6 +118,17 @@ Devices with the final size of *0* will be discarded.
117118

118119
Defaults to *discard*.
119120

121+
## DIRECTIVES
122+
123+
The global section (before any section header) may contain directives in the following form:
124+
125+
* `set!`*variable*=*program*
126+
127+
*program* is executed by the shell as-if by system(3),
128+
its standard output stream parsed as an arithmetic expression (like `zram-size`/`zram-resident-limit`),
129+
then the result is remembered into *variable*,
130+
usable in later `set!`s and `zram-size`s/`zram-resident-limit`s.
131+
120132
## ENVIRONMENT VARIABLES
121133

122134
Setting `ZRAM_GENERATOR_ROOT` during parsing will cause */proc/meminfo* to be read from *$ZRAM_GENERATOR_ROOT/proc/meminfo* instead,

src/config.rs

Lines changed: 94 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ use std::ffi::OsString;
1010
use std::fmt;
1111
use std::fs;
1212
use std::io::{prelude::*, BufReader};
13+
use std::os::unix::process::ExitStatusExt;
1314
use std::path::{Component, Path, PathBuf};
15+
use std::process::{Command, Stdio};
1416

1517
const DEFAULT_ZRAM_SIZE: &str = "min(ram / 2, 4096)";
1618
const DEFAULT_RESIDENT_LIMIT: &str = "0";
@@ -97,14 +99,14 @@ impl Device {
9799
fn process_size(
98100
&self,
99101
zram_option: &Option<(String, fasteval::ExpressionI, fasteval::Slab)>,
100-
memtotal_mb: f64,
102+
ctx: &mut EvalContext,
101103
default_size: f64,
102104
label: &str,
103105
) -> Result<u64> {
104106
Ok((match zram_option {
105107
Some(zs) => {
106108
zs.1.from(&zs.2.ps)
107-
.eval(&zs.2, &mut RamNs(memtotal_mb))
109+
.eval(&zs.2, ctx)
108110
.with_context(|| format!("{} {}", self.name, label))
109111
.and_then(|f| {
110112
if f >= 0. {
@@ -119,29 +121,29 @@ impl Device {
119121
* 1024.0) as u64)
120122
}
121123

122-
fn set_disksize_if_enabled(&mut self, memtotal_mb: u64) -> Result<()> {
123-
if !self.is_enabled(memtotal_mb) {
124+
fn set_disksize_if_enabled(&mut self, ctx: &mut EvalContext) -> Result<()> {
125+
if !self.is_enabled(ctx.memtotal_mb) {
124126
return Ok(());
125127
}
126128

127129
if self.zram_fraction.is_some() || self.max_zram_size_mb.is_some() {
128130
// deprecated path
129131
let max_mb = self.max_zram_size_mb.unwrap_or(None).unwrap_or(u64::MAX);
130-
self.disksize = ((self.zram_fraction.unwrap_or(0.5) * memtotal_mb as f64) as u64)
132+
self.disksize = ((self.zram_fraction.unwrap_or(0.5) * ctx.memtotal_mb as f64) as u64)
131133
.min(max_mb)
132134
* (1024 * 1024);
133135
} else {
134136
self.disksize = self.process_size(
135137
&self.zram_size,
136-
memtotal_mb as f64,
137-
(memtotal_mb as f64 / 2.).min(4096.), // DEFAULT_ZRAM_SIZE
138+
ctx,
139+
(ctx.memtotal_mb as f64 / 2.).min(4096.), // DEFAULT_ZRAM_SIZE
138140
"zram-size",
139141
)?;
140142
}
141143

142144
self.mem_limit = self.process_size(
143145
&self.zram_resident_limit,
144-
memtotal_mb as f64,
146+
ctx,
145147
0., // DEFAULT_RESIDENT_LIMIT
146148
"zram-resident-limit",
147149
)?;
@@ -225,13 +227,19 @@ impl fmt::Display for Algorithms {
225227
}
226228
}
227229

228-
struct RamNs(f64);
229-
impl fasteval::EvalNamespace for RamNs {
230+
struct EvalContext {
231+
memtotal_mb: u64,
232+
additional: BTreeMap<String, f64>,
233+
}
234+
235+
impl fasteval::EvalNamespace for EvalContext {
230236
fn lookup(&mut self, name: &str, args: Vec<f64>, _: &mut String) -> Option<f64> {
231-
if name == "ram" && args.is_empty() {
232-
Some(self.0)
233-
} else {
237+
if !args.is_empty() {
234238
None
239+
} else if name == "ram" {
240+
Some(self.memtotal_mb as f64)
241+
} else {
242+
self.additional.get(name).copied()
235243
}
236244
}
237245
}
@@ -252,6 +260,57 @@ pub fn read_all_devices(root: &Path, kernel_override: bool) -> Result<Vec<Device
252260
.collect())
253261
}
254262

263+
fn toplevel_line(
264+
path: &Path,
265+
k: &str,
266+
val: &str,
267+
slab: &mut fasteval::Slab,
268+
ctx: &mut EvalContext,
269+
) -> Result<()> {
270+
let (op, arg) = if let Some(colon) = k.find('!') {
271+
k.split_at(colon + 1)
272+
} else {
273+
warn!(
274+
"{}: invalid outside-of-section key {}, ignoring.",
275+
path.display(),
276+
k
277+
);
278+
return Ok(());
279+
};
280+
281+
match op {
282+
"set!" => {
283+
let out = Command::new("/bin/sh")
284+
.args(["-c", "--", val])
285+
.stdin(Stdio::null())
286+
.stderr(Stdio::inherit())
287+
.output()
288+
.with_context(|| format!("{}: {}: {}", path.display(), k, val))?;
289+
let exit = out
290+
.status
291+
.code()
292+
.unwrap_or_else(|| 128 + out.status.signal().unwrap());
293+
if exit != 0 {
294+
warn!("{}: {} exited {}", k, val, exit);
295+
}
296+
297+
let expr = String::from_utf8(out.stdout)
298+
.with_context(|| format!("{}: {}: {}", path.display(), k, val))?;
299+
let evalled = fasteval::Parser::new()
300+
.parse(&expr, &mut slab.ps)
301+
.and_then(|p| p.from(&slab.ps).eval(slab, ctx))
302+
.with_context(|| format!("{}: {}: {}: {}", path.display(), k, val, expr))?;
303+
ctx.additional.insert(arg.to_string(), evalled);
304+
}
305+
_ => warn!(
306+
"{}: unknown outside-of-section operation {}, ignoring.",
307+
path.display(),
308+
op
309+
),
310+
}
311+
Ok(())
312+
}
313+
255314
fn read_devices(
256315
root: &Path,
257316
kernel_override: bool,
@@ -264,18 +323,21 @@ fn read_devices(
264323
}
265324

266325
let mut devices: HashMap<String, Device> = HashMap::new();
326+
let mut slab = fasteval::Slab::new();
327+
let mut ctx = EvalContext {
328+
memtotal_mb,
329+
additional: BTreeMap::new(),
330+
};
267331

268332
for (_, path) in fragments {
269333
let ini = Ini::load_from_file(&path)?;
270334

271335
for (sname, props) in ini.iter() {
272336
let sname = match sname {
273337
None => {
274-
warn!(
275-
"{}: ignoring settings outside of section: {:?}",
276-
path.display(),
277-
props
278-
);
338+
for (k, v) in props.iter() {
339+
toplevel_line(&path, k, v, &mut slab, &mut ctx)?;
340+
}
279341
continue;
280342
}
281343
Some(sname) if sname.starts_with("zram") && sname[4..].parse::<u64>().is_ok() => {
@@ -304,7 +366,7 @@ fn read_devices(
304366
}
305367

306368
for dev in devices.values_mut() {
307-
dev.set_disksize_if_enabled(memtotal_mb)?;
369+
dev.set_disksize_if_enabled(&mut ctx)?;
308370
}
309371

310372
Ok(devices)
@@ -624,7 +686,11 @@ foo=0
624686
parse_line(&mut dev, "zram-size", val).unwrap();
625687
}
626688
assert!(dev.is_enabled(memtotal_mb));
627-
dev.set_disksize_if_enabled(memtotal_mb).unwrap();
689+
dev.set_disksize_if_enabled(&mut EvalContext {
690+
memtotal_mb,
691+
additional: vec![("two".to_string(), 2.)].into_iter().collect(),
692+
})
693+
.unwrap();
628694
dev.disksize
629695
}
630696

@@ -636,6 +702,14 @@ foo=0
636702
);
637703
}
638704

705+
#[test]
706+
fn test_eval_size_expression_with_additional() {
707+
assert_eq!(
708+
dev_with_zram_size_size(Some("0.5 * ram * two"), 100),
709+
50 * 2 * 1024 * 1024
710+
);
711+
}
712+
639713
#[test]
640714
fn test_eval_size_expression_500() {
641715
assert_eq!(
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1+
set!top = echo 3
2+
set!bottom = echo 4
3+
set!ratio = ! echo top / bottom
4+
15
[zram0]
26
compression-algorithm = zstd
37
host-memory-limit = 2050
4-
zram-size = ram * 0.75
8+
zram-resident-limit = 9999
9+
zram-size = ram * ratio

tests/10-example/bin/xenstore-read

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#!/bin/sh -x
2+
echo '8 * 1024' # MB

tests/test_cases.rs

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@ use zram_generator::{config, generator};
44

55
use anyhow::Result;
66
use fs_extra::dir::{copy, CopyOptions};
7+
use std::env;
8+
use std::ffi::OsString;
79
use std::fs;
810
use std::io::{self, Write};
11+
use std::os::unix::ffi::OsStringExt;
912
use std::path::Path;
1013
use std::process::{exit, Command};
1114
use tempfile::TempDir;
@@ -43,6 +46,15 @@ fn unshorn() {
4346
.unwrap();
4447
fs::create_dir("/proc/self").unwrap();
4548
symlink("zram-generator", "/proc/self/exe").unwrap();
49+
50+
let mut path = env::var_os("PATH")
51+
.map(|p| p.to_os_string().into_vec())
52+
.unwrap_or(b"/usr/bin:/bin".to_vec()); // _PATH_DEFPATH
53+
path.insert(0, b':');
54+
for &b in "tests/10-example/bin".as_bytes().into_iter().rev() {
55+
path.insert(0, b);
56+
}
57+
env::set_var("PATH", OsString::from_vec(path));
4658
}
4759

4860
fn prepare_directory(srcroot: &Path) -> Result<TempDir> {
@@ -114,6 +126,9 @@ fn test_01_basic() {
114126
assert_eq!(d.host_memory_limit_mb, None);
115127
assert_eq!(d.zram_size.as_ref().map(z_s_name), None);
116128
assert_eq!(d.options, "discard");
129+
130+
assert_eq!(d.disksize, 391 * 1024 * 1024);
131+
assert_eq!(d.mem_limit, 0);
117132
}
118133

119134
#[test]
@@ -123,7 +138,7 @@ fn test_02_zstd() {
123138
let d = &devices[0];
124139
assert!(d.is_swap());
125140
assert_eq!(d.host_memory_limit_mb, Some(2050));
126-
assert_eq!(d.zram_size.as_ref().map(z_s_name), Some("ram * 0.75"));
141+
assert_eq!(d.zram_size.as_ref().map(z_s_name), Some("ram * ratio"));
127142
assert_eq!(
128143
d.compression_algorithms,
129144
config::Algorithms {
@@ -132,6 +147,9 @@ fn test_02_zstd() {
132147
}
133148
);
134149
assert_eq!(d.options, "discard");
150+
151+
assert_eq!(d.disksize, 782 * 1024 * 1024 * 3 / 4);
152+
assert_eq!(d.mem_limit, 9999 * 1024 * 1024);
135153
}
136154

137155
#[test]
@@ -153,11 +171,17 @@ fn test_04_dropins() {
153171
assert_eq!(d.host_memory_limit_mb, Some(1235));
154172
assert_eq!(d.zram_size.as_ref().map(z_s_name), None);
155173
assert_eq!(d.options, "discard");
174+
175+
assert_eq!(d.disksize, 782 * 1024 * 1024 / 2);
176+
assert_eq!(d.mem_limit, 0);
156177
}
157178
"zram2" => {
158179
assert_eq!(d.host_memory_limit_mb, None);
159180
assert_eq!(d.zram_size.as_ref().map(z_s_name), Some("ram*0.8"));
160181
assert_eq!(d.options, "");
182+
183+
assert_eq!(d.disksize, 782 * 1024 * 1024 * 8 / 10);
184+
assert_eq!(d.mem_limit, 0);
161185
}
162186
_ => panic!("Unexpected device {}", d),
163187
}
@@ -306,12 +330,26 @@ fn test_10_example() {
306330
}
307331
);
308332
assert_eq!(d.options, "");
333+
334+
assert_eq!(
335+
d.zram_resident_limit.as_ref().map(z_s_name),
336+
Some("maxhotplug * 3/4")
337+
);
338+
339+
assert_eq!(d.disksize, 782 * 1024 * 1024 / 10);
340+
// This is the combination of tests/10-example/bin/xenstore-read and
341+
// zram-resident-limit= in tests/10-example/etc/systemd/zram-generator.conf.
342+
assert_eq!(d.mem_limit, 8 * 1024 * 1024 * 1024 * 3 / 4);
309343
}
344+
310345
"zram1" => {
311346
assert_eq!(d.fs_type.as_ref().unwrap(), "ext2");
312347
assert_eq!(d.effective_fs_type(), "ext2");
313348
assert_eq!(d.zram_size.as_ref().map(z_s_name), Some("ram / 10"));
314349
assert_eq!(d.options, "discard");
350+
351+
assert_eq!(d.disksize, 782 * 1024 * 1024 / 10);
352+
assert_eq!(d.mem_limit, 0);
315353
}
316354
_ => panic!("Unexpected device {}", d),
317355
}

zram-generator.conf.example

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
# This file is part of the zram-generator project
22
# https://github.com/systemd/zram-generator
33

4+
# At the top level, a set!variable = program
5+
# directive executes /bin/sh -c program,
6+
# parses the output as an expression, and remembers it in variable,
7+
# usable in later set! and zram-size/zram-resident-limit.
8+
set!maxhotplug = xenstore-read /local/domain/$(xenstore-read domid)/memory/hotplug-max
9+
410
[zram0]
511
# This section describes the settings for /dev/zram0.
612
#
@@ -25,7 +31,7 @@ zram-size = min(ram / 10, 2048)
2531
# then this device will not consume more than 128 MiB.
2632
#
2733
# 0 means no limit; this is the default.
28-
zram-resident-limit = 0
34+
zram-resident-limit = maxhotplug * 3/4
2935

3036
# The compression algorithm to use for the zram device,
3137
# or leave unspecified to keep the kernel default.

0 commit comments

Comments
 (0)