Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 0 additions & 13 deletions client_lib/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,14 @@ use serde::{Deserialize, Serialize};
use std::cmp;
use std::fs::File;
use std::io::Read;
use std::time::Duration;
use yaml_rust::YamlLoader;

#[derive(Debug, Serialize, Deserialize)]
pub enum SnapshotManagement {
/// Always render the latest snapshot.
Single,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct ClientConfig {
/// Hostname of the machine running the controller.
pub server_hostname: String,
/// Virtual video channel to listen to.
pub video_channel: u64,
/// UNUSED - preserved until client machines are updated
pub render_delay: Duration,
/// UNUSED - preserved until client machines are updated
pub snapshot_management: SnapshotManagement,
pub x_resolution: u32,
pub y_resolution: u32,
/// If true, set the window to fullscreen on creation.
Expand Down Expand Up @@ -61,7 +50,6 @@ impl ClientConfig {
ClientConfig {
server_hostname: host,
video_channel,
render_delay: Default::default(),
x_resolution,
y_resolution,
fullscreen,
Expand All @@ -72,7 +60,6 @@ impl ClientConfig {
y_center: f64::from(y_resolution / 2),
transformation,
log_level_debug,
snapshot_management: SnapshotManagement::Single,
}
}

Expand Down
28 changes: 24 additions & 4 deletions scripts/build-app.sh
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,14 @@ for bin in tunnelclient tunnel-bootstrap bootstrap-deploy; do
chmod +x "$APP/Contents/MacOS/$bin"
done

# Helper script for viewing logs.
cat > "$APP/Contents/MacOS/view-logs.sh" <<'LOGSCRIPT'
# Helper script for viewing logs. Lives in Resources/ rather than MacOS/
# because Contents/MacOS/ is conventionally Mach-O-only and codesign
# refuses non-Mach-O subcomponents there when signing without --deep.
cat > "$APP/Contents/Resources/view-logs.sh" <<'LOGSCRIPT'
#!/bin/bash
log stream --predicate 'subsystem == "com.generalelectrix.tunnels"'
LOGSCRIPT
chmod +x "$APP/Contents/MacOS/view-logs.sh"
chmod +x "$APP/Contents/Resources/view-logs.sh"

cp "$ICNS" "$APP/Contents/Resources/Tunnels.icns"
cp "$PROJECT_DIR/controller_templates/tunnels.touchosc" "$APP/Contents/Resources/tunnels.touchosc"
Expand Down Expand Up @@ -111,8 +113,26 @@ cat > "$APP/Contents/Info.plist" <<PLIST
</plist>
PLIST

echo "==> Signing helper binaries..."
# Each helper gets its own ad-hoc identity. tunnelclient especially must NOT
# inherit the bundle's CFBundleIdentifier — it gets pushed to remote render
# machines by tunnel-bootstrap and runs there free-standing, without the
# bundle's Info.plist context. Identifying as the bundle there causes macOS
# TCC's Local Network privacy gate to silently deny its outbound LAN
# connection to the show host. (See v2026.04.18-1 regression.)
for bin in tunnelclient tunnel-bootstrap bootstrap-deploy; do
codesign -s - --force \
--identifier "com.generalelectrix.tunnels.$bin" \
"$APP/Contents/MacOS/$bin"
done

echo "==> Signing app bundle..."
codesign -s - --force --deep --identifier com.generalelectrix.tunnels "$APP"
# Bundle-level sign handles the main executable AND the resources
# manifest. No --deep — helpers are pre-signed above with their own
# identifiers, and we want to preserve those.
codesign -s - --force \
--identifier com.generalelectrix.tunnels \
"$APP"

echo "==> Creating DMG..."
BG_PNG="$PROJECT_DIR/dist/dmg-background.png"
Expand Down
45 changes: 36 additions & 9 deletions tunnelclient/src/show.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
use anyhow::{Result, anyhow};
use client_lib::config::ClientConfig;
use graphics::clear;
use graphics::{CircleArc, Context, clear};
use log::{error, info};
use opengl_graphics::{GlGraphics, OpenGL};
use piston_window::prelude::*;
use sdl2_window::Sdl2Window;
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::{Duration, Instant};
use tunnelclient::draw::Draw;
use tunnels_lib::RunFlag;
use tunnels_lib::Snapshot;
Expand All @@ -21,6 +22,8 @@ pub struct Show {
cfg: ClientConfig,
run_flag: RunFlag,
window: PistonWindow<Sdl2Window>,
/// Reference instant for animating the waiting-for-snapshot spinner.
start_time: Instant,
}

impl Show {
Expand Down Expand Up @@ -57,6 +60,7 @@ impl Show {
cfg,
run_flag,
window,
start_time: Instant::now(),
})
}

Expand All @@ -78,21 +82,44 @@ impl Show {
}

/// Render a frame to the window.
///
/// Always clears to black, then either draws the latest snapshot's
/// layers or — if no snapshot has arrived yet — a small spinner
/// indicating the client is up and waiting. The unconditional clear
/// is what keeps an unfed client from showing uninitialized GPU
/// memory as static gray noise.
fn render(&mut self, args: &RenderArgs) {
let Some(snapshot) = self.snapshot_manager.lock().unwrap().clone() else {
return;
};

let snapshot = self.snapshot_manager.lock().unwrap().clone();
self.gl.draw(args.viewport(), |c, gl| {
// Clear the screen.
clear([0.0, 0.0, 0.0, 1.0], gl);

// Draw everything.
snapshot.layers.draw(&c, gl, &self.cfg);
match snapshot {
Some(snapshot) => snapshot.layers.draw(&c, gl, &self.cfg),
None => draw_waiting_spinner(&c, gl, &self.cfg, self.start_time.elapsed()),
}
});
}
}

/// Draw a small dark-gray rotating arc at screen center as a "this client
/// is alive but hasn't received a snapshot yet" indicator.
fn draw_waiting_spinner(c: &Context, gl: &mut GlGraphics, cfg: &ClientConfig, elapsed: Duration) {
use std::f64::consts::{PI, TAU};
let cx = f64::from(cfg.x_resolution) / 2.0;
let cy = f64::from(cfg.y_resolution) / 2.0;
let radius = 20.0;
let thickness = 2.0;
// One revolution every 2 seconds.
let phase = elapsed.as_secs_f64() * 0.5 * TAU;
let arc = 1.5 * PI; // 270°
let bounds = [cx - radius, cy - radius, radius * 2.0, radius * 2.0];
CircleArc::new([0.25, 0.25, 0.25, 1.0], thickness, phase, phase + arc).draw(
bounds,
&c.draw_state,
c.transform,
gl,
);
}

/// Spawn a thread to receive snapshots.
/// Inject them into the provided manager.
/// The thread runs until the run flag is tripped.
Expand Down