mirror of
https://github.com/SteamFramelyHomebrew/framely.git
synced 2026-10-24 21:01:53 +02:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b32f7102a | ||
|
|
9f99ecdfa8 | ||
|
|
9a72a50f1a | ||
|
|
1d6f5a0e3f |
No files matched your search
Generated
+1
-1
@@ -425,7 +425,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "framely"
|
||||
version = "0.8.4"
|
||||
version = "0.8.5-preview.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "framely"
|
||||
version = "0.8.4"
|
||||
version = "0.8.5-preview.1"
|
||||
edition = "2021"
|
||||
license = "AGPL-3.0-only"
|
||||
|
||||
|
||||
@@ -221,7 +221,7 @@ cat > /etc/systemd/system/framely-session.service <<EOF
|
||||
[Unit]
|
||||
Description=Framely SteamVR UI session
|
||||
After=framely.service user@$steam_uid.service
|
||||
Requires=framely.service
|
||||
Requires=framely.service user@$steam_uid.service
|
||||
[Service]
|
||||
User=$steam_user
|
||||
Environment=HOME=$steam_home
|
||||
|
||||
+44
-1
@@ -2276,11 +2276,35 @@ fn backup(home: &Path, c: &Container, db: &Database, log: &Path) -> Result<Strin
|
||||
fs::create_dir(&d)?;
|
||||
let dest = d.join("baked");
|
||||
fs::create_dir(&dest)?;
|
||||
for entry in fs::read_dir(&c.baked)?.flatten() {
|
||||
for entry in fs::read_dir(&c.baked)? {
|
||||
let entry = entry?;
|
||||
let name = entry.file_name();
|
||||
if name.to_string_lossy().ends_with("_workdir") {
|
||||
continue;
|
||||
}
|
||||
// Native launches keep their OverlayFS upper/work pair here. The
|
||||
// kernel-owned work/work can be unreadable even after stopping.
|
||||
let native_overlay = name.to_str().and_then(|n| n.strip_prefix("native-apk-"))
|
||||
.is_some_and(|id| id.len() == 24 && id.bytes().all(|b| b.is_ascii_hexdigit()));
|
||||
if native_overlay && entry.file_type()?.is_dir() {
|
||||
let target = dest.join(&name);
|
||||
fs::create_dir(&target)?;
|
||||
for child in fs::read_dir(entry.path())? {
|
||||
let child = child?;
|
||||
if child.file_name() == "work" {
|
||||
ensure!(child.file_type()?.is_dir(), "Invalid native OverlayFS work directory");
|
||||
// Include a fresh work directory in the verified snapshot,
|
||||
// preserving all persistent data (including upper).
|
||||
let work = target.join("work");
|
||||
fs::create_dir(&work)?;
|
||||
fs::set_permissions(work, fs::Permissions::from_mode(0o700))?;
|
||||
} else {
|
||||
copy(&child.path(), &target.join(child.file_name()), log)?;
|
||||
}
|
||||
}
|
||||
fs::set_permissions(&target, fs::metadata(entry.path())?.permissions())?;
|
||||
continue;
|
||||
}
|
||||
copy(&entry.path(), &dest.join(name), log)?;
|
||||
}
|
||||
|
||||
@@ -6558,7 +6582,22 @@ esac
|
||||
c.baked.join("data_overlay/media/0"),
|
||||
)
|
||||
.unwrap();
|
||||
let native_name = format!("native-apk-{}", hash("com.example.app"));
|
||||
let native = c.baked.join(&native_name);
|
||||
fs::create_dir_all(native.join("upper/files/work")).unwrap();
|
||||
fs::write(native.join("upper/files/work/save"), b"native progress").unwrap();
|
||||
std::os::unix::fs::symlink("files/work/save", native.join("upper/save-link")).unwrap();
|
||||
fs::create_dir_all(native.join("work/work")).unwrap();
|
||||
fs::write(native.join("work/work/transient"), b"kernel scratch").unwrap();
|
||||
fs::set_permissions(native.join("work/work"), fs::Permissions::from_mode(0)).unwrap();
|
||||
fs::create_dir_all(c.baked.join("app_workdir/work")).unwrap();
|
||||
fs::set_permissions(c.baked.join("app_workdir/work"), fs::Permissions::from_mode(0)).unwrap();
|
||||
let id = backup(&f.home, &c, &db, &f.dir.path().join("test.log")).unwrap();
|
||||
let saved = root(&f.home).join("backups").join(&id).join("baked");
|
||||
assert_eq!(fs::read_dir(saved.join(&native_name).join("work")).unwrap().count(), 0);
|
||||
assert!(!saved.join("app_workdir").exists());
|
||||
assert_eq!(fs::read(saved.join(&native_name).join("upper/save-link")).unwrap(), b"native progress");
|
||||
verified_snapshot(saved.parent().unwrap()).unwrap();
|
||||
assert!(clean_candidates(&f.home, &db)
|
||||
.unwrap()
|
||||
.iter()
|
||||
@@ -6566,6 +6605,7 @@ esac
|
||||
fs::write(f.save_file(), "changed progress").unwrap();
|
||||
fs::write(&resource, b"changed download").unwrap();
|
||||
fs::write(&shader, b"changed shader").unwrap();
|
||||
fs::write(native.join("upper/files/work/save"), b"changed native progress").unwrap();
|
||||
let scratch = c.baked.join("data_workdir/work");
|
||||
fs::create_dir_all(&scratch).unwrap();
|
||||
fs::set_permissions(&scratch, fs::Permissions::from_mode(0)).unwrap();
|
||||
@@ -6579,6 +6619,9 @@ esac
|
||||
assert_eq!(fs::read_to_string(f.save_file()).unwrap(), "saved progress");
|
||||
assert_eq!(fs::read(&resource).unwrap(), b"unfinished download");
|
||||
assert_eq!(fs::read(&shader).unwrap(), b"compiled shader");
|
||||
assert_eq!(fs::read(native.join("upper/save-link")).unwrap(), b"native progress");
|
||||
assert_eq!(fs::read_dir(native.join("work")).unwrap().count(), 0);
|
||||
assert_eq!(fs::metadata(native.join("work")).unwrap().permissions().mode() & 0o777, 0o700);
|
||||
assert_eq!(
|
||||
fs::read(
|
||||
c.baked
|
||||
|
||||
+10
-6
@@ -52,13 +52,14 @@ fn runtime() -> Result<PathBuf> {
|
||||
let uid = unsafe { libc::geteuid() };
|
||||
ensure!(uid != 0, "Steam APK wrapper must run as the session user");
|
||||
let p = PathBuf::from(format!("/run/user/{uid}/framely-apk-steam"));
|
||||
fs::create_dir_all(&p)?;
|
||||
let m = fs::symlink_metadata(&p)?;
|
||||
fs::create_dir_all(&p).with_context(|| format!("Create Steam APK runtime directory {}", p.display()))?;
|
||||
let m = fs::symlink_metadata(&p).with_context(|| format!("Inspect Steam APK runtime directory {}", p.display()))?;
|
||||
ensure!(
|
||||
m.is_dir() && m.uid() == uid,
|
||||
"Invalid Steam wrapper runtime directory"
|
||||
);
|
||||
fs::set_permissions(&p, fs::Permissions::from_mode(0o700))?;
|
||||
fs::set_permissions(&p, fs::Permissions::from_mode(0o700))
|
||||
.with_context(|| format!("Protect Steam APK runtime directory {}", p.display()))?;
|
||||
Ok(p)
|
||||
}
|
||||
fn socket() -> Result<PathBuf> {
|
||||
@@ -503,7 +504,8 @@ pub(super) fn start_session() -> Result<()> {
|
||||
.create(true)
|
||||
.truncate(false)
|
||||
.mode(0o600)
|
||||
.open(dir.join("server.lock"))?;
|
||||
.open(dir.join("server.lock"))
|
||||
.with_context(|| format!("Open Steam APK bridge lock {}", dir.join("server.lock").display()))?;
|
||||
ensure!(
|
||||
unsafe { libc::flock(lock.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) } == 0,
|
||||
"Steam APK bridge is already running"
|
||||
@@ -516,8 +518,10 @@ pub(super) fn start_session() -> Result<()> {
|
||||
);
|
||||
fs::remove_file(&path)?;
|
||||
}
|
||||
let listener = UnixListener::bind(&path)?;
|
||||
fs::set_permissions(path, fs::Permissions::from_mode(0o600))?;
|
||||
let listener = UnixListener::bind(&path)
|
||||
.with_context(|| format!("Bind Steam APK bridge socket {}", path.display()))?;
|
||||
fs::set_permissions(&path, fs::Permissions::from_mode(0o600))
|
||||
.with_context(|| format!("Protect Steam APK bridge socket {}", path.display()))?;
|
||||
std::thread::spawn(move || {
|
||||
let _lock = lock;
|
||||
let workers = Arc::new(std::sync::atomic::AtomicUsize::new(0));
|
||||
|
||||
+27
-5
@@ -536,10 +536,16 @@ impl Agent {
|
||||
if let Some(id)=views["screenshotClosed"].as_str().filter(|id|!id.is_empty()) {
|
||||
self.screenshot.lock().unwrap().acknowledge(id);
|
||||
}
|
||||
if views["views"].is_object() {
|
||||
self.core("host.ui.visibility",views["views"].clone())?;
|
||||
if views.get("views").is_some() {
|
||||
// Null is the startup state before the first VR frame:
|
||||
// leave visibility unknown rather than reporting hidden.
|
||||
ensure!(views["views"].is_null() || views["views"].is_object(), "Invalid native visibility snapshot");
|
||||
if views["views"].is_object() {
|
||||
self.core("host.ui.visibility",views["views"].clone())?;
|
||||
}
|
||||
if let Some(ids)=views["closedCasts"].as_array(){ensure!(ids.len()<=32,"Too many closed casts");for id in ids{if let Some(id)=id.as_str(){let _=self.receivers.lock().unwrap().control(id,"stop",json!({}));}}}
|
||||
} else if views.is_object() {
|
||||
ensure!(views.as_object().unwrap().values().all(Value::is_boolean), "Invalid legacy visibility report");
|
||||
self.core("host.ui.visibility", views)?;
|
||||
}
|
||||
}
|
||||
@@ -2520,7 +2526,7 @@ pub fn serve(
|
||||
"UI session must not run as root"
|
||||
);
|
||||
crate::gamepad::session_started();
|
||||
crate::apk::steam_session_started()?;
|
||||
crate::apk::steam_session_started().context("Initialize Steam APK session bridge")?;
|
||||
crate::apk::lifecycle_started();
|
||||
let server = Server::http("127.0.0.1:0").map_err(|e| anyhow::anyhow!(e.to_string()))?;
|
||||
let port = server
|
||||
@@ -2536,9 +2542,10 @@ pub fn serve(
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| PathBuf::from("/tmp"));
|
||||
let runtime = runtime.join(format!("framely-session-{}", std::process::id()));
|
||||
fs::create_dir(&runtime)?;
|
||||
fs::create_dir(&runtime).with_context(|| format!("Create UI runtime directory {}", runtime.display()))?;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
fs::set_permissions(&runtime, fs::Permissions::from_mode(0o700))?;
|
||||
fs::set_permissions(&runtime, fs::Permissions::from_mode(0o700))
|
||||
.with_context(|| format!("Protect UI runtime directory {}", runtime.display()))?;
|
||||
let agent = Arc::new(Agent {
|
||||
watch_access: Mutex::new(crate::cast::watch_access::Access {
|
||||
token: ipc::call(&socket, "cast.watchAccess.get", json!({}))?["token"].as_str().map(str::to_owned),
|
||||
@@ -4036,6 +4043,9 @@ mod tests {
|
||||
let socket_worker = std::thread::spawn(move || {
|
||||
let mut core = crate::service::Service::load(&root.path().join("state"), 1000).unwrap();
|
||||
for expected in [
|
||||
"host.notification.badge",
|
||||
"status",
|
||||
"launcher.gaze.calibration.get",
|
||||
"host.ui.visibility",
|
||||
"host.notification.badge",
|
||||
"status",
|
||||
@@ -4074,6 +4084,18 @@ mod tests {
|
||||
.set("Cookie", &format!("framely={}", agent.web_key))
|
||||
.send_json(json!({"menu":true}))
|
||||
.is_err());
|
||||
// The first native poll precedes the first VR visibility snapshot.
|
||||
// It must succeed without treating envelope fields as boolean views.
|
||||
let startup: Value = ureq::post(&url)
|
||||
.set("X-Framely-Native", &agent.native_key)
|
||||
.send_json(json!({"views":null,"closedCasts":[],"stopRecording":"","framingClosed":"","screenshotClosed":"","screenshotHotkey":"","radialAction":""}))
|
||||
.unwrap().into_json().unwrap();
|
||||
assert_eq!(startup["commands"],json!([framing]));
|
||||
agent.commands.lock().unwrap().push_back(framing.clone());
|
||||
for invalid in [json!({"views":[],"closedCasts":[]}),json!({"menu":[],"closedCasts":[]})] {
|
||||
assert!(ureq::post(&url).set("X-Framely-Native", &agent.native_key)
|
||||
.send_json(invalid).is_err());
|
||||
}
|
||||
let response = ureq::post(&url)
|
||||
.set("X-Framely-Native", &agent.native_key)
|
||||
.send_json(json!({"menu":true}))
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
"""Check the actual installed UI unit waits for the user runtime provider."""
|
||||
import configparser
|
||||
import pathlib
|
||||
import subprocess
|
||||
import unittest
|
||||
|
||||
|
||||
class SessionUnitTests(unittest.TestCase):
|
||||
def test_session_requires_and_orders_after_user_manager(self):
|
||||
script = (pathlib.Path(__file__).parents[1] / 'packaging/install.sh').read_text()
|
||||
body = script.split('cat > /etc/systemd/system/framely-session.service <<EOF\n', 1)[1].split('\nEOF', 1)[0]
|
||||
result = subprocess.run(
|
||||
['bash', '-eu', '-c', 'steam_uid=1000; steam_user=steamos; steam_home=/home/steamos; root=/var/lib/framely\ncat <<EOF\n' + body + '\nEOF\n'],
|
||||
capture_output=True, text=True, check=True,
|
||||
)
|
||||
unit = configparser.ConfigParser(interpolation=None, strict=False)
|
||||
unit.read_string(result.stdout)
|
||||
for dependency in ['framely.service', f'user@{1000}.service']:
|
||||
self.assertIn(dependency, unit['Unit']['Requires'].split())
|
||||
self.assertIn(dependency, unit['Unit']['After'].split())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user