Skip to content

refactor: pulseaudio server management and suspend behavior#1574

Merged
utkarshdalal merged 3 commits into
utkarshdalal:masterfrom
joshuatam:feat/refactor-pulseaudio-server
Jun 12, 2026
Merged

refactor: pulseaudio server management and suspend behavior#1574
utkarshdalal merged 3 commits into
utkarshdalal:masterfrom
joshuatam:feat/refactor-pulseaudio-server

Conversation

@joshuatam

@joshuatam joshuatam commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Description

Remove the pulseaudioSuspendBehavior preference and its related process signal handling.

The PulseAudio server is now consistently run as a daemon, and audio suspension/resumption is managed by dynamically suspending and resuming the module-aaudio-sink via pactl.

This simplifies the audio component's lifecycle and improves stability by leveraging standard PulseAudio control mechanisms.

Recording

Type of Change

  • Bug fix
  • Performance / stability improvement
  • Compatibility improvements
  • Other (requires prior approval)

Checklist

  • If I have access to #code-changes, I have discussed this change there and it has been green-lighted. If I do not have access, I have still provided clear context in this PR. If I skip both, I accept that this change may face delays in review, may not be reviewed at all, or may be closed.
  • This change aligns with the current project scope (core functionality, stability, or performance). If not, it has been explicitly approved beforehand.
  • I have attached a recording of the change.
  • I have read and agree to the contribution guidelines in CONTRIBUTING.md.

Summary by cubic

Run PulseAudio as a daemon and control pause/resume by toggling pactl suspend-sink on AAudioSink. Removes the pulseaudioSuspendBehavior setting and process-signal logic for a simpler, more stable audio flow.

  • Refactors
    • PulseAudioComponent: no process tracking or timers; server check via pactl info; pause/resume uses pactl suspend-sink; low-latency stays.
    • Start PulseAudio with --daemonize=true, auth-cookie-enabled=false, clean ~/.config, set PULSE_SERVER, load via default.pa; refreshed asset to pulseaudio-gamenative-20260612.tzst.
    • Removed pulseaudioSuspendBehavior from prefs, UI, Container, and serialization; XServerScreen now passes only low-latency.
    • ProcessHelper: removed PID and duplicate exec helpers; consolidated execWithOutput with better logging.

Written for commit 0799d06. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes

    • Removed the PulseAudio "suspend behavior" option from audio preferences — General tab no longer shows that setting.
  • Chores

    • Simplified PulseAudio handling for more immediate pause/resume and more reliable startup/shutdown.
    • Packaged PulseAudio components updated; low-latency audio toggle remains supported and is applied directly.

Remove the `pulseaudioSuspendBehavior` preference and its related process signal handling.

The PulseAudio server is now consistently run as a daemon, and audio suspension/resumption is managed by dynamically loading and unloading the `module-aaudio-sink` via `pactl`.

This simplifies the audio component's lifecycle and improves stability by leveraging standard PulseAudio control mechanisms.
@joshuatam joshuatam requested a review from utkarshdalal as a code owner June 11, 2026 18:58
@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Removes the configurable pulseaudioSuspendBehavior setting across prefs, UI, container model, and refactors PulseAudioComponent to daemon-mode startup with pactl-based liveness checks, immediate pactl suspend/resume, and a simplified constructor using only socket config and low-latency flag.

Changes

PulseAudio Suspend Behavior Removal and Lifecycle Refactor

Layer / File(s) Summary
Remove pulseaudioSuspendBehavior from configuration and persistence
app/src/main/java/app/gamenative/PrefManager.kt, app/src/main/java/app/gamenative/ui/component/dialog/GeneralTab.kt, app/src/main/java/com/winlator/container/Container.java, app/src/main/java/com/winlator/container/ContainerData.kt
Removes the pulseaudioSuspendBehavior preference and UI dropdown, deletes Container field/getter/setter and its JSON serialization/deserialization, and removes ContainerData property and its save/restore mappings; pulseaudioLowLatency remains.
Update ProcessHelper for command execution and output capture
app/src/main/java/com/winlator/core/ProcessHelper.java
Removes reflective getPid(Process), consolidates execWithOutput to the four-parameter variant with includeStderr, and adds a debug log that prints command parts, env, and working directory before running commands.
Simplify PulseAudioComponent constructor and imports
app/src/main/java/com/winlator/xenvironment/components/PulseAudioComponent.java
Removes suspend-behavior constants/fields and timer/module tracking, updates imports to use AtomicBoolean, and changes the public constructor to (UnixSocketConfig socketConfig, boolean lowLatency).
Refactor PulseAudio startup and lifecycle management
app/src/main/java/com/winlator/xenvironment/components/PulseAudioComponent.java
Adds startPulseAudio() to prepare working dir, generate updated default.pa (disabling auth-cookie), and start libpulseaudio.so in daemon mode via ProcessHelper.execWithOutput(..., true); replaces stored-process lifecycle with kill/start flows; simplifies stop(), pause(), and resume() to use pactl suspend-sink and process orchestration; updates execPactlCommand()/isServerRunning() to return and parse pactl output.
Update component wiring and asset version
app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt
Passes container.pulseaudioLowLatency into PulseAudioComponent during environment setup and updates pulseaudio component asset version from pulseaudio-gamenative-20260609.tzst to pulseaudio-gamenative-20260612.tzst.

Sequence Diagram

sequenceDiagram
  participant XServerScreen
  participant PulseAudioComponent
  participant ProcessHelper
  participant System

  XServerScreen->>PulseAudioComponent: new PulseAudioComponent(socketConfig, lowLatency)
  PulseAudioComponent->>PulseAudioComponent: isServerRunning() (execPactlCommand -> ProcessHelper)
  PulseAudioComponent->>ProcessHelper: execWithOutput("pactl info", ..., true)
  ProcessHelper->>System: spawn pactl, capture output
  System-->>ProcessHelper: pactl output
  ProcessHelper-->>PulseAudioComponent: command output
  PulseAudioComponent->>System: killall pulseaudio / start libpulseaudio.so via ProcessHelper
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • utkarshdalal

Poem

🐰 I nibbled a setting, trimmed a key with care,
Daemonized the pulses, made the soundscape fair,
Pactl whispers check the server's state,
Pause and resume now happen straight,
A hopping rabbit hums — code clean as air. 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: removing pulseaudioSuspendBehavior and refactoring PulseAudio server management, which aligns with the primary objectives across multiple files.
Description check ✅ Passed The description provides context on the change, specifies the type of change, and confirms completion of checklist items, though the recording checkbox is unchecked.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/src/main/java/com/winlator/core/ProcessHelper.java`:
- Line 200: The current Log.d call in ProcessHelper that prints
Arrays.toString(splitCommand(command)) and Arrays.toString(envp) leaks secrets;
change the logging in the method that contains splitCommand(command) so it only
logs the executable name (derived from splitCommand(command)[0] or the command
string before the first space) and a redacted/whitelisted env map (e.g., include
only SAFE_KEYS like "PATH","HOME" and replace other keys/values with
"<REDACTED>"); do not print full argv or raw envp arrays and ensure any helper
used for formatting (e.g., splitCommand) is used only to extract the executable
name.

In
`@app/src/main/java/com/winlator/xenvironment/components/PulseAudioComponent.java`:
- Around line 163-172: The code currently treats isModuleLoaded as authoritative
but it’s only intent; change the logic so the flag is only flipped after
verifying the module/sink actually exists—do not set isModuleLoaded
unconditionally inside loadModule() and unloadModule(); instead, have
loadModule() verify success (e.g., confirm pactl returned a module id or that
isSinkAlive() becomes true) and then set isModuleLoaded = true, and have
unloadModule() confirm removal before setting isModuleLoaded = false; also
update the branch that uses isModuleLoaded (the block calling isSinkAlive(),
loadModule(), and updateSink(false)) to prefer a real check (call isSinkAlive()
or verify module presence) when deciding to attempt reload so failed pactl calls
don’t leave the flag true and prevent retries.
- Around line 96-98: The component wrongly assumes PulseAudio is up because
startPulseAudio() ignores subprocess failures and isServerRunning() treats any
output other than "connection refused" as healthy; update startPulseAudio() to
return a boolean (or throw) based on the actual process exit status and
stdout/stderr (or verify a successful pactl info), change isServerRunning() to
only report healthy when a positive success signal is observed (e.g., pactl info
returns expected fields or pid/socket exists), and then in start() (and the
other call sites that currently call killAllPulseAudioProcesses();
startPulseAudio();) use the boolean/exception to decide whether the server is
truly running and retry/abort the restart path when startPulseAudio() failed.
Ensure killAllPulseAudioProcesses(), startPulseAudio(), isServerRunning(), and
start() are updated accordingly so failures are propagated and logged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9434d940-c502-4091-8efa-ea80ebd84a79

📥 Commits

Reviewing files that changed from the base of the PR and between 3b72ac7 and ed5e01e.

📒 Files selected for processing (10)
  • app/src/main/assets/pulseaudio-gamenative-20260609.tzst
  • app/src/main/assets/pulseaudio-gamenative-20260612.tzst
  • app/src/main/java/app/gamenative/PrefManager.kt
  • app/src/main/java/app/gamenative/ui/component/dialog/GeneralTab.kt
  • app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt
  • app/src/main/java/app/gamenative/utils/ContainerUtils.kt
  • app/src/main/java/com/winlator/container/Container.java
  • app/src/main/java/com/winlator/container/ContainerData.kt
  • app/src/main/java/com/winlator/core/ProcessHelper.java
  • app/src/main/java/com/winlator/xenvironment/components/PulseAudioComponent.java
💤 Files with no reviewable changes (5)
  • app/src/main/java/app/gamenative/PrefManager.kt
  • app/src/main/java/app/gamenative/ui/component/dialog/GeneralTab.kt
  • app/src/main/java/com/winlator/container/ContainerData.kt
  • app/src/main/java/com/winlator/container/Container.java
  • app/src/main/java/app/gamenative/utils/ContainerUtils.kt

Comment thread app/src/main/java/com/winlator/core/ProcessHelper.java
Comment thread app/src/main/java/com/winlator/xenvironment/components/PulseAudioComponent.java Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 issues found across 10 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread app/src/main/java/com/winlator/xenvironment/components/PulseAudioComponent.java Outdated
Comment thread app/src/main/java/com/winlator/core/ProcessHelper.java
Comment thread app/src/main/java/com/winlator/xenvironment/components/PulseAudioComponent.java Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/src/main/java/com/winlator/xenvironment/components/PulseAudioComponent.java (1)

107-117: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't gate daemon recovery on isPaused alone.

If pause() hits the isServerRunning() guard while the daemon is already down, it leaves isPaused=false. XEnvironment.onResume() still uses resume() as the audio recovery hook, but this method immediately returns in that state, so PulseAudio never gets restarted after the app comes back to foreground.

Possible minimal fix
 public void resume() {
     synchronized (lock) {
+        if (!isServerRunning()) {
+            start();
+            return;
+        }
+
         if (isPaused.get()) {
             Timber.tag("PulseAudioComponent").d("Resuming...");
-
-            if (isServerRunning()) {
-                // Set isPaused immediately
-                isPaused.set(false);
-                updateSink(false);
-
-                Timber.tag("PulseAudioComponent").d("Audio resumed");
-            } else {
-                start();
-            }
+            isPaused.set(false);
+            updateSink(false);
+            Timber.tag("PulseAudioComponent").d("Audio resumed");
         }
     }
 }

Also applies to: 121-135

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@app/src/main/java/com/winlator/xenvironment/components/PulseAudioComponent.java`
around lines 107 - 117, The pause() method should not leave isPaused false when
the daemon is already down: set isPaused.set(true) unconditionally before
checking isServerRunning(), then only call updateSink(true) and the "Pausing..."
log if the server is running; keep the existing guarded behavior for
updateSink/isServerRunning but ensure the paused flag is always true so
XEnvironment.onResume()/resume() can perform recovery; apply the same
unconditional isPaused set behavior to the analogous block in the 121-135
region.
🧹 Nitpick comments (1)
app/src/main/java/com/winlator/xenvironment/components/PulseAudioComponent.java (1)

20-38: ⚡ Quick win

Refresh the class docblock.

It still describes timer-based suspend modes, SIGSTOP/SIGCONT handling, and module unload/reload behavior that no longer exists in this class. Leaving it in place makes the new lifecycle much harder to reason about.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@app/src/main/java/com/winlator/xenvironment/components/PulseAudioComponent.java`
around lines 20 - 38, The class docblock for PulseAudioComponent is outdated and
describes timer-based suspend modes and SIGSTOP/SIGCONT/pactl behavior that no
longer exist; replace it with a short, accurate summary of the current lifecycle
and responsibilities of PulseAudioComponent, removing references to timers,
SIGSTOP/SIGCONT, and module unload/reload. Mention the key state and API
surfaces to guide readers (e.g., isPaused field, updateSink(...) method, and any
public lifecycle methods such as start(), stop(), pause(), resume()) and note
any important side-effects (threading assumptions or I/O interactions) so the
docblock reflects the actual implementation and usage.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In
`@app/src/main/java/com/winlator/xenvironment/components/PulseAudioComponent.java`:
- Around line 107-117: The pause() method should not leave isPaused false when
the daemon is already down: set isPaused.set(true) unconditionally before
checking isServerRunning(), then only call updateSink(true) and the "Pausing..."
log if the server is running; keep the existing guarded behavior for
updateSink/isServerRunning but ensure the paused flag is always true so
XEnvironment.onResume()/resume() can perform recovery; apply the same
unconditional isPaused set behavior to the analogous block in the 121-135
region.

---

Nitpick comments:
In
`@app/src/main/java/com/winlator/xenvironment/components/PulseAudioComponent.java`:
- Around line 20-38: The class docblock for PulseAudioComponent is outdated and
describes timer-based suspend modes and SIGSTOP/SIGCONT/pactl behavior that no
longer exist; replace it with a short, accurate summary of the current lifecycle
and responsibilities of PulseAudioComponent, removing references to timers,
SIGSTOP/SIGCONT, and module unload/reload. Mention the key state and API
surfaces to guide readers (e.g., isPaused field, updateSink(...) method, and any
public lifecycle methods such as start(), stop(), pause(), resume()) and note
any important side-effects (threading assumptions or I/O interactions) so the
docblock reflects the actual implementation and usage.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 714122ac-d0fc-4e94-b0b0-1eed76ca8f22

📥 Commits

Reviewing files that changed from the base of the PR and between 58691c9 and 0799d06.

📒 Files selected for processing (1)
  • app/src/main/java/com/winlator/xenvironment/components/PulseAudioComponent.java

@phobos665 phobos665 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting. So we're removing a lot of complexity and instead leveraging the daemon instead. Nice.

@utkarshdalal utkarshdalal merged commit a14d949 into utkarshdalal:master Jun 12, 2026
3 checks passed
@joshuatam joshuatam deleted the feat/refactor-pulseaudio-server branch June 12, 2026 16:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants