Skip to content
Open
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
14 changes: 14 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
name: Lint
run-name: ${{ github.actor }} is running lint checks 🔍
on: [push, pull_request]
jobs:
platform-paths:
runs-on: ubuntu-latest
steps:
- name: Check out repository code
uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Shared code must not hardcode platform paths
run: python scripts/check_platform_paths.py
48 changes: 46 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Cross-platform native desktop application and screensaver for infinidream.ai (ma
```
client_generic/
MacBuild/
e-dream.xcodeproj # Xcode project
infinidream.xcodeproj # Xcode project
build.py # Build script (-r release, -s stage, -n notarize, -v version)
release.py # Publish release with Sparkle appcast generation
MSVC/
Expand All @@ -44,7 +44,7 @@ Shared setup (all platforms): clone with submodules (`git submodule update --ini
```bash
brew install git-lfs && git lfs install # Required for binary assets
./vcpkg/bootstrap-vcpkg.sh && ./vcpkg/vcpkg install # Install C++ deps
open client_generic/MacBuild/e-dream.xcodeproj # Open in Xcode
open client_generic/MacBuild/infinidream.xcodeproj # Open in Xcode
cd client_generic/MacBuild && ./build.py # Build app (Debug)
cd client_generic/MacBuild && ./build.py -r -n # Release build with notarization
cd client_generic/MacBuild && ./release.py -v X.Y.Z # Publish release (appcast)
Expand Down Expand Up @@ -82,6 +82,50 @@ Linux has no UI sign-in: authenticate with `INFINIDREAM_API_KEY` env var or `~/.
- Code signing: macOS auto-discovers Developer ID from Keychain; Windows uses Authenticode (`release.py --sign`)
- Auto-update via Sparkle appcast XML — macOS only; Windows/Linux ship via GitHub releases without in-app update

## Platform-Specific Code

**Shared code asks for platform facts; it never states them.** A hardcoded
`~/.config/infinidream/settings.json` in shared code compiles cleanly on all
three platforms and is wrong on two of them, with no build-time signal — that is
how [#675](https://github.com/e-dream-ai/client/issues/675) happened.

The seam is `client_generic/Client/PlatformUtils.h`: one header of `static`
declarations, with exactly one implementation compiled per build system.

| Platform | Implementation | Wired into |
|---|---|---|
| Windows | `client_generic/MSVC/PlatformUtils_win.cpp` | `MSVC/electricsheep.vcxproj` |
| macOS | `client_generic/MacBuild/PlatformUtils_Mac.mm` | `MacBuild/infinidream.xcodeproj` |
| Linux | `client_generic/LinuxBuild/PlatformUtils_Linux.cpp` | `LinuxBuild/CMakeLists.txt` |

Because the declarations are unconditional, **adding a method and forgetting an
implementation is a link error on that platform** — loud and immediate. That is
the property an `#ifdef` cannot give you: a missing or wrong `#ifdef` branch
fails silently at runtime, usually in a user's log.

Rules:

- Need a platform-specific value or behaviour in shared code? Add a
`PlatformUtils` method and implement it in **all three** files. Where a
platform has nothing to do, write an explicit no-op with a comment saying why.
- Never add an `#ifdef WIN32` / `MAC` / `LINUX_GNU` to `PlatformUtils.h` itself.
Platform-private helpers go in that platform's own internal header — see
`LinuxBuild/PlatformUtils_Internal.h`.
- For the settings file location, use `g_Settings()->ConfigPath()`. It returns
the path the client actually opened, so it cannot drift from reality.
- To put an error in front of a user, call `g_Log->Error()`. It already forwards
to `PlatformUtils::NotifyError()` (see `CLog::Error` in `Common/Log.cpp`), so
calling the seam directly as well reports the same failure twice.
`NotifyError()` is currently a `TODO` stub on Windows and Mac — the log file is
the only channel that reaches a user there.
- `#ifdef` is still fine for `#include`s and for genuinely platform-shaped APIs
(a function taking an `HWND`). It is not fine for facts that shared code could
have asked for.

`scripts/check_platform_paths.py` enforces the path half of this in CI
(`.github/workflows/lint.yml`). Run it locally with
`python scripts/check_platform_paths.py`.

## Runtime Logs

Runtime logs are written to one file per day (`YYYY_MM_DD.log`) in:
Expand Down
13 changes: 7 additions & 6 deletions client_generic/Client/PlatformUtils.h
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,13 @@ class PlatformUtils
static void NotifyError(std::string_view errorMessage);
static std::string CalculateFileMD5(const std::string& filepath);

#ifdef WIN32
/// HWND for SetTimer-based delayed work and input hooks (registered from the player window).
static void Win32SetMessageWindow(void* hwnd);
/// Called from display WM_MOUSEMOVE (client coordinates).
static void NotifyMouseMoved(int x, int y);
#endif
/// Native window handle used for delayed work and input hooks, registered from
/// the player window (an HWND on Windows). Platforms that route those through
/// other means implement this as a no-op.
static void SetNativeMessageWindow(void* _nativeHandle);
/// Called from the display's mouse-move event, in client coordinates.
/// Platforms that deliver mouse moves directly implement this as a no-op.
static void NotifyMouseMoved(int _x, int _y);
};

class CDelayedDispatch
Expand Down
2 changes: 1 addition & 1 deletion client_generic/Client/Player.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,7 @@ void CPlayer::SetHWND(HWND _hWnd)
if (m_hWnd == nullptr)
{
m_hWnd = _hWnd;
PlatformUtils::Win32SetMessageWindow(static_cast<void*>(_hWnd));
PlatformUtils::SetNativeMessageWindow(static_cast<void*>(_hWnd));
}
}
#endif
Expand Down
94 changes: 74 additions & 20 deletions client_generic/Client/client.h
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ class CElectricSheep
std::string m_PreviousDlState; // Track download status
bool m_MultipleInstancesMode;
bool m_CachedOnlyMode = false;
bool m_CachedOnlyModeChecked = false;
bool m_StartFullscreen = false; // Linux: start fullscreen (--fullscreen); default is windowed
bool m_OfflineDueToNoInternetOnly = false; // true when m_MultipleInstancesMode was set only because internet was down (don't show Busy in that case)
bool m_bConfigMode;
Expand Down Expand Up @@ -827,6 +828,55 @@ class CElectricSheep
}
}

// --cached: play locally cached videos without a session. Parsed and honoured
// on every platform, so this lives outside any platform guard. Idempotent
// (m_CachedOnlyModeChecked) because Windows and Linux both call it early,
// before creating any window -- an empty cache should exit with no window ever
// appearing, but each of those platforms' own Startup() creates its display
// before reaching the shared Startup() below, so they call this first and bail
// out ahead of that. Mac only reaches it from Startup() below.
bool CheckCachedOnlyMode()
{
if (m_CachedOnlyModeChecked)
return true;
m_CachedOnlyModeChecked = true;

if (!m_CachedOnlyMode)
return true;

// Set this up front rather than after the cache check below. It gates
// EDreamClient::InitializeClient(), which is what keeps the auth thread —
// and so the sign-in wizard — out of an explicitly offline session. The
// failure path returns early, so assigning it last meant a --cached run
// with an empty cache still started auth during teardown and told the
// user to "run with --cached", which is what they had just done.
m_MultipleInstancesMode = true; // forces offline mode through the rest of Startup()

Cache::CacheManager& cmPre = Cache::CacheManager::getInstance();
cmPre.loadDiskCachedFromJson();
size_t cachedCount = cmPre.getCachedDreamCount();
double cachedGB = cmPre.getCacheSize();
if (cachedCount == 0 && cachedGB < 0.001)
{
// Error() hands the message to PlatformUtils::NotifyError() as well as
// the log (see CLog::Error) — that is the platform seam for putting a
// failure in front of a user: stderr on Linux, a TODO stub on Windows
// and Mac. So this one call both records the failure and delivers it to
// whatever those stubs grow into. Until they grow something, the log
// file is all a Windows user has: the client is a GUI binary there,
// with no console attached.
g_Log->Error("--cached requested but no cached videos found. "
"Run without --cached first to download content.");
return false;
}

g_Log->Info("--cached: no sealed session token, cycling through %zu (%.1f GB) of cached videos.",
cachedCount, cachedGB);
printf("No sealed session token — cycling through %zu (%.1f GB) of cached videos.\n",
cachedCount, cachedGB);
return true;
}

//
virtual bool Startup()
{
Expand All @@ -851,24 +901,18 @@ class CElectricSheep
(CPlayer::MultiDisplayMode)g_Settings()->Get(
"settings.player.MultiDisplayMode", 0));

#ifdef LINUX_GNU
// Linux/headless: pre-check auth / cache state before the window opens.
// --cached: play locally cached videos without a session. Parsed and honoured
// on every platform, so keep this branch out of any platform guard. Windows
// and Linux already ran this (see CheckCachedOnlyMode()) before creating
// their window; the call is then a no-op. Mac hits it here for the first time.
if (m_CachedOnlyMode)
{
// --cached: play locally cached videos without a session.
Cache::CacheManager& cmPre = Cache::CacheManager::getInstance();
cmPre.loadDiskCachedFromJson();
size_t cachedCount = cmPre.getCachedDreamCount();
double cachedGB = cmPre.getCacheSize();
if (cachedCount == 0 && cachedGB < 0.001)
{
fprintf(stderr, "No cached videos found. Run without --cached first to download content.\n");
if (!CheckCachedOnlyMode())
return false;
}
printf("No sealed session token — cycling through %zu (%.1f GB) of cached videos.\n",
cachedCount, cachedGB);
m_MultipleInstancesMode = true; // forces offline mode through the rest of Startup()
}
#ifdef LINUX_GNU
// Linux/headless only: the console magic-link flow, before the window opens.
// Mac and Windows reach their own GUI sign-in wizard instead.
else
{
std::string sealedSession = g_Settings()->Get("settings.content.sealed_session", std::string(""));
Expand Down Expand Up @@ -980,8 +1024,11 @@ class CElectricSheep

if (m_MultipleInstancesMode) {
g_Player().SetOfflineMode(true);
// Show busy indicator only for actual multiple instances, not when offline due to no internet
if (!IsPreview() && !m_OfflineDueToNoInternetOnly) {
// Show busy indicator only for actual multiple instances -- not when
// offline due to no internet, and not for --cached, which also forces
// m_MultipleInstancesMode true to get offline behaviour but isn't a
// second instance at all; "Busy" would misleadingly suggest one.
if (!IsPreview() && !m_OfflineDueToNoInternetOnly && !m_CachedOnlyMode) {
m_BusyIndicatorEndTime = m_Timer.Time() + 30.0;
}
}
Expand Down Expand Up @@ -1895,9 +1942,13 @@ class CElectricSheep
// Show standalone indicator HUD only when credits overlay is hidden
// and any indicator timer is active (hide all in preview mode)
bool showDisk = m_DiskIndicatorEndTime > 0.0 && !IsPreview();
bool showBusy = m_BusyIndicatorEndTime > 0.0 && !IsPreview();
// --cached never starts the auth thread or a WebSocket connection
// (that's the point of offline mode), so it would otherwise read as
// a permanent "Busy"/"Remote" problem rather than the intended,
// expected offline state. Suppress both here too.
bool showBusy = m_BusyIndicatorEndTime > 0.0 && !IsPreview() && !m_CachedOnlyMode;
bool showNet = m_NetworkIndicatorEndTime > 0.0 && !IsPreview();
bool showRemote = m_RemoteIndicatorEndTime > 0.0 && !IsPreview() && !inInitialAuthWindow;
bool showRemote = m_RemoteIndicatorEndTime > 0.0 && !IsPreview() && !inInitialAuthWindow && !m_CachedOnlyMode;
bool showUpdate = m_UpdateIndicatorEndTime > 0.0;

bool shouldShowIndicatorHUD = !creditsVisible &&
Expand Down Expand Up @@ -2016,9 +2067,12 @@ class CElectricSheep
bool updateAvailable = m_RuntimeDiagnostics.updateAvailable;

bool showDisk = diskSpaceLow && !IsPreview();
showBusy = m_MultipleInstancesMode && !m_OfflineDueToNoInternetOnly && !IsPreview();
// Same --cached suppression as the standalone indicator HUD above:
// offline mode here is intentional (no auth thread, no WebSocket),
// not a stuck "Busy"/"Remote" state worth flagging to the user.
showBusy = m_MultipleInstancesMode && !m_OfflineDueToNoInternetOnly && !IsPreview() && !m_CachedOnlyMode;
showNet = !internetConnected && !IsPreview();
showRemote = !wsConnected && !IsPreview() && !inInitialAuthWindow;
showRemote = !wsConnected && !IsPreview() && !inInitialAuthWindow && !m_CachedOnlyMode;
showUpdate = updateAvailable;

// Disk needs spacing for: Busy (if shown) + Net (if shown) + Remote (if shown) + Update (if shown)
Expand Down
8 changes: 8 additions & 0 deletions client_generic/Client/client_linux.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,14 @@ class CElectricSheep_Linux : public CElectricSheep
std::string tmp = "Working dir: " + m_WorkingDir;
g_Log->Info(tmp.c_str());

// Check --cached against the disk cache here, before AddDisplay() below
// creates a window -- an empty cache should exit with no window ever
// appearing. Needs InitStorage()/AttachLog() above to have run, so the
// failure reaches the log. CElectricSheep::Startup() re-checks this, but
// CheckCachedOnlyMode() is a no-op once already run.
if (m_CachedOnlyMode && !CheckCachedOnlyMode())
return false;

// Run gui.

// Start windowed by default; --fullscreen (m_StartFullscreen) opts into
Expand Down
17 changes: 17 additions & 0 deletions client_generic/Client/client_win32.h
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,15 @@ class CElectricSheep_Win32 : public CElectricSheep
m_ScrMode = eFullScreenStandalone;
m_bAllowFKey = true;
}
else
{
// Unrecognized flag -- e.g. --cached, which main() already parsed
// separately via CommandLineToArgvW into m_CachedOnlyMode. Leaving
// m_ScrMode at eNone here fails the mode check below and aborts
// Startup() before CElectricSheep::Startup() (where the --cached
// logic lives) ever runs, so fall back to windowed mode instead.
m_ScrMode = eWindowed;
}
}

// Check for multiple instances if we're not specifically asked not
Expand Down Expand Up @@ -430,6 +439,14 @@ class CElectricSheep_Win32 : public CElectricSheep

_chdir(m_WorkingDir.c_str());

// Check --cached against the disk cache now, before any window/display is
// created below -- an empty cache should exit with no window ever
// appearing. CElectricSheep::Startup() (called further down) re-checks
// this too, for Mac/Linux, but CheckCachedOnlyMode() is a no-op there
// once already run here.
if (m_CachedOnlyMode && !CheckCachedOnlyMode())
return false;

// Mirror the Mac behavior: when the user-facing app launches, reassert
// ourselves as the active screensaver if the preference is on. Done
// here (not in the installer) so HKCU resolves to the actual user, not
Expand Down
18 changes: 17 additions & 1 deletion client_generic/Client/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
#ifdef WIN32
#include <process.h>
#include <windows.h>
#include <shellapi.h> // CommandLineToArgvW — must follow windows.h
#include <cwchar>
#pragma comment(lib, "shell32.lib")
#endif
#include <float.h>
#include <signal.h>
Expand All @@ -35,7 +38,20 @@ typedef CElectricSheep_Linux CElectricSheepClient;
int32_t APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nCmdShow)
{
bool cachedOnlyMode = false; // WIN32 does not support --cached flag parsing yet
// lpCmdLine is ANSI and untokenised. CommandLineToArgvW splits the wide command
// line with the same quoting rules the CRT gives argv, so we get argv semantics
// without hand-rolling a parser. Screensaver flags (/s, /c, /p) are parsed
// separately in client_win32.h off GetCommandLineA(); this only wants --cached.
bool cachedOnlyMode = false;
int wideArgCount = 0;
LPWSTR* wideArgs = CommandLineToArgvW(GetCommandLineW(), &wideArgCount);
if (wideArgs != nullptr)
{
for (int i = 1; i < wideArgCount; ++i)
if (wcscmp(wideArgs[i], L"--cached") == 0) { cachedOnlyMode = true; break; }

LocalFree(wideArgs);
}
#else
int32_t main(int argc, char* argv[])
{
Expand Down
4 changes: 4 additions & 0 deletions client_generic/LinuxBuild/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,7 @@ deps-src/
openssl-minimal/
ffmpeg-minimal/
*.AppImage

# Bytecode from build_appimage.py, same as WinBuild/__pycache__/ in the root
# .gitignore. Anyone who runs the AppImage build generates it.
__pycache__/
7 changes: 7 additions & 0 deletions client_generic/LinuxBuild/PlatformUtils_Linux.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,13 @@ std::function<void(int, int)>& PlatformUtils_GetMouseCallback()
return s_mouseCallback;
}

// No-ops on Linux: there is no HWND to register, and CDisplayVulkan invokes the
// mouse-moved callback directly from the Wayland/X11 event loop rather than
// routing through PlatformUtils.
void PlatformUtils::SetNativeMessageWindow(void* /*_nativeHandle*/) {}

void PlatformUtils::NotifyMouseMoved(int /*_x*/, int /*_y*/) {}

// ---------------------------------------------------------------------------
// Thread name
// ---------------------------------------------------------------------------
Expand Down
4 changes: 2 additions & 2 deletions client_generic/MSVC/PlatformUtils_win.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,9 @@ std::string RunCommandAndGetFirstLine(const char* command)
}
} // namespace

void PlatformUtils::Win32SetMessageWindow(void* hwnd)
void PlatformUtils::SetNativeMessageWindow(void* _nativeHandle)
{
g_win32MsgHwnd = static_cast<HWND>(hwnd);
g_win32MsgHwnd = static_cast<HWND>(_nativeHandle);
}

void PlatformUtils::NotifyMouseMoved(int x, int y)
Expand Down
6 changes: 6 additions & 0 deletions client_generic/MacBuild/PlatformUtils_Mac.mm
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,12 @@
}
}

// No-ops on Mac: there is no HWND to register, and SetOnMouseMovedCallback above
// installs an NSEvent monitor that delivers mouse moves directly.
void PlatformUtils::SetNativeMessageWindow(void* /*_nativeHandle*/) {}

void PlatformUtils::NotifyMouseMoved(int /*_x*/, int /*_y*/) {}

void PlatformUtils::OpenURLExternally(std::string_view _url)
{
NSString* str = [[NSString alloc] initWithBytes:_url.data()
Expand Down
Loading
Loading