mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-01-23 02:36:05 +00:00
refactor(electron): check for best idle method only once
This commit is contained in:
parent
595a396d81
commit
051bb5dd90
1 changed files with 155 additions and 142 deletions
|
|
@ -13,145 +13,172 @@ type IdleDetectionMethod =
|
|||
| 'loginctl'
|
||||
| 'none';
|
||||
|
||||
interface EnvironmentInfo {
|
||||
readonly isWayland: boolean;
|
||||
readonly isGnomeWayland: boolean;
|
||||
readonly isSnap: boolean;
|
||||
readonly sessionType?: string;
|
||||
readonly currentDesktop?: string;
|
||||
readonly waylandDisplay?: string;
|
||||
readonly gnomeShellSession?: string;
|
||||
}
|
||||
|
||||
interface IdleMethodCandidate {
|
||||
readonly name: IdleDetectionMethod;
|
||||
readonly test: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
const ERROR_LOG_INTERVAL = 60000;
|
||||
|
||||
export class IdleTimeHandler {
|
||||
private isWayland: boolean = false;
|
||||
private isGnomeWayland: boolean = false;
|
||||
private workingMethod: IdleDetectionMethod = 'none';
|
||||
private hasTestedMethods: boolean = false;
|
||||
private isSnapEnvironment: boolean = false;
|
||||
private lastErrorLog: number = 0;
|
||||
private ERROR_LOG_INTERVAL = 60000; // Log errors at most once per minute
|
||||
private readonly _environment: EnvironmentInfo;
|
||||
private _methodDetectionPromise: Promise<IdleDetectionMethod> | null = null;
|
||||
private _workingMethod: IdleDetectionMethod = 'none';
|
||||
private _lastErrorLog = 0;
|
||||
|
||||
constructor() {
|
||||
this.detectEnvironment();
|
||||
this._environment = this._detectEnvironment();
|
||||
this._methodDetectionPromise = this._initializeWorkingMethod();
|
||||
}
|
||||
|
||||
private detectEnvironment(): void {
|
||||
private _detectEnvironment(): EnvironmentInfo {
|
||||
const sessionType = process.env.XDG_SESSION_TYPE;
|
||||
const currentDesktop = process.env.XDG_CURRENT_DESKTOP;
|
||||
const waylandDisplay = process.env.WAYLAND_DISPLAY;
|
||||
const gnomeShellVersion = process.env.GNOME_SHELL_SESSION_MODE;
|
||||
const gnomeShellSession = process.env.GNOME_SHELL_SESSION_MODE;
|
||||
|
||||
this.isWayland = sessionType === 'wayland' || !!waylandDisplay;
|
||||
this.isGnomeWayland =
|
||||
this.isWayland &&
|
||||
(currentDesktop?.toLowerCase().includes('gnome') ||
|
||||
currentDesktop?.toLowerCase().includes('ubuntu') || // Ubuntu uses GNOME
|
||||
!!gnomeShellVersion);
|
||||
const isWayland = sessionType === 'wayland' || !!waylandDisplay;
|
||||
const normalizedDesktop = currentDesktop?.toLowerCase() ?? '';
|
||||
const isGnomeDesktop =
|
||||
normalizedDesktop.includes('gnome') ||
|
||||
normalizedDesktop.includes('ubuntu') ||
|
||||
!!gnomeShellSession;
|
||||
const isGnomeWayland = isWayland && isGnomeDesktop;
|
||||
const isSnap = !!process.env.SNAP || !!process.env.SNAP_NAME;
|
||||
|
||||
// Check if running in snap environment
|
||||
this.isSnapEnvironment = !!process.env.SNAP || !!process.env.SNAP_NAME;
|
||||
|
||||
log.info('Environment detection:', {
|
||||
const environment: EnvironmentInfo = {
|
||||
isWayland,
|
||||
isGnomeWayland,
|
||||
isSnap,
|
||||
sessionType,
|
||||
currentDesktop,
|
||||
waylandDisplay,
|
||||
gnomeShellVersion,
|
||||
isWayland: this.isWayland,
|
||||
isGnomeWayland: this.isGnomeWayland,
|
||||
isSnapEnvironment: this.isSnapEnvironment,
|
||||
});
|
||||
gnomeShellSession,
|
||||
};
|
||||
|
||||
log.info('Environment detection:', environment);
|
||||
return environment;
|
||||
}
|
||||
|
||||
private async testIdleDetectionMethods(): Promise<void> {
|
||||
if (this.hasTestedMethods) {
|
||||
return;
|
||||
private _initializeWorkingMethod(): Promise<IdleDetectionMethod> {
|
||||
const detection = this._determineWorkingMethod()
|
||||
.then((method) => {
|
||||
this._workingMethod = method;
|
||||
return method;
|
||||
})
|
||||
.catch((error) => {
|
||||
log.warn('Idle detection initialization failed', error);
|
||||
this._workingMethod = 'none';
|
||||
return 'none';
|
||||
});
|
||||
|
||||
detection.then((method) => log.info(`Idle detection method ready: ${method}`));
|
||||
|
||||
return detection;
|
||||
}
|
||||
|
||||
private async _ensureWorkingMethod(): Promise<IdleDetectionMethod> {
|
||||
if (!this._methodDetectionPromise) {
|
||||
this._methodDetectionPromise = this._initializeWorkingMethod();
|
||||
}
|
||||
return this._methodDetectionPromise;
|
||||
}
|
||||
|
||||
private async _determineWorkingMethod(): Promise<IdleDetectionMethod> {
|
||||
log.info('Determining idle detection method...');
|
||||
|
||||
if (!this._environment.isWayland) {
|
||||
log.info('Using powerMonitor for non-Wayland session');
|
||||
return 'powerMonitor';
|
||||
}
|
||||
|
||||
log.info('Testing idle detection methods (one-time initialization)...');
|
||||
|
||||
if (!this.isWayland) {
|
||||
// For X11, we can rely on powerMonitor
|
||||
this.workingMethod = 'powerMonitor';
|
||||
log.info('X11 detected, will use powerMonitor for idle detection');
|
||||
} else {
|
||||
// Test methods in order of preference for Wayland
|
||||
const methods: Array<{ name: IdleDetectionMethod; test: () => Promise<boolean> }> =
|
||||
[
|
||||
{
|
||||
name: 'gnomeDBus',
|
||||
test: async () => {
|
||||
if (!this.isGnomeWayland) return false;
|
||||
try {
|
||||
const idleTime = await this.getGnomeIdleTime();
|
||||
return idleTime !== null;
|
||||
} catch (error) {
|
||||
if (this.isSnapEnvironment) {
|
||||
log.info('GNOME DBus test failed in snap environment:', error);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'xprintidle',
|
||||
test: async () => {
|
||||
try {
|
||||
const idleTime = await this.getXprintidleTime();
|
||||
return idleTime !== null;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'loginctl',
|
||||
test: async () => {
|
||||
// Skip loginctl in snap environment due to AppArmor restrictions
|
||||
if (this.isSnapEnvironment) {
|
||||
log.info('Skipping loginctl in snap environment');
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const idleTime = await this.getLoginctlIdleTime();
|
||||
return idleTime !== null;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
for (const method of methods) {
|
||||
try {
|
||||
log.info(`Testing ${method.name}...`);
|
||||
const works = await method.test();
|
||||
if (works) {
|
||||
this.workingMethod = method.name;
|
||||
log.info(
|
||||
`Successfully tested ${method.name} - will use this for idle detection`,
|
||||
);
|
||||
break;
|
||||
} else {
|
||||
log.info(`${method.name} test failed, trying next method`);
|
||||
}
|
||||
} catch (error) {
|
||||
log.warn(`${method.name} test error:`, error);
|
||||
for (const candidate of this._buildWaylandCandidates()) {
|
||||
log.info(`Testing ${candidate.name}...`);
|
||||
try {
|
||||
const works = await candidate.test();
|
||||
if (works) {
|
||||
log.info(`Selected ${candidate.name} for idle detection`);
|
||||
return candidate.name;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.workingMethod === 'none') {
|
||||
log.warn(
|
||||
'No working idle detection method found for Wayland. Idle detection will be disabled.',
|
||||
);
|
||||
log.info(`${candidate.name} test failed`);
|
||||
} catch (error) {
|
||||
log.warn(`${candidate.name} test error`, error);
|
||||
}
|
||||
}
|
||||
|
||||
this.hasTestedMethods = true;
|
||||
log.warn(
|
||||
'No working idle detection method found for Wayland. Idle detection will be disabled.',
|
||||
);
|
||||
return 'none';
|
||||
}
|
||||
|
||||
private _buildWaylandCandidates(): IdleMethodCandidate[] {
|
||||
const candidates: IdleMethodCandidate[] = [];
|
||||
|
||||
if (this._environment.isGnomeWayland) {
|
||||
candidates.push({
|
||||
name: 'gnomeDBus',
|
||||
test: async () => {
|
||||
try {
|
||||
const idleTime = await this._getGnomeIdleTime();
|
||||
return idleTime !== null;
|
||||
} catch (error) {
|
||||
if (this._environment.isSnap) {
|
||||
log.info('GNOME DBus test failed in snap environment', error);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
candidates.push({
|
||||
name: 'xprintidle',
|
||||
test: async () => {
|
||||
try {
|
||||
const idleTime = await this._getXprintidleTime();
|
||||
return idleTime !== null;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (this._environment.isSnap) {
|
||||
log.info('Skipping loginctl in snap environment');
|
||||
} else {
|
||||
candidates.push({
|
||||
name: 'loginctl',
|
||||
test: async () => {
|
||||
try {
|
||||
const idleTime = await this._getLoginctlIdleTime();
|
||||
return idleTime !== null;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return candidates;
|
||||
}
|
||||
|
||||
async getIdleTime(): Promise<number> {
|
||||
// Ensure methods have been tested
|
||||
if (!this.hasTestedMethods) {
|
||||
await this.testIdleDetectionMethods();
|
||||
}
|
||||
|
||||
const methodUsed = this.workingMethod;
|
||||
const methodUsed = await this._ensureWorkingMethod();
|
||||
let idleTime = 0;
|
||||
let error: unknown = null;
|
||||
|
||||
// Use the working method determined during initialization
|
||||
switch (this.workingMethod) {
|
||||
switch (methodUsed) {
|
||||
case 'powerMonitor':
|
||||
try {
|
||||
idleTime = powerMonitor.getSystemIdleTime() * 1000;
|
||||
|
|
@ -164,7 +191,7 @@ export class IdleTimeHandler {
|
|||
|
||||
case 'gnomeDBus':
|
||||
try {
|
||||
const result = await this.getGnomeIdleTime();
|
||||
const result = await this._getGnomeIdleTime();
|
||||
idleTime = result ?? 0;
|
||||
log.debug(`gnomeDBus idle time: ${idleTime}ms`);
|
||||
if (result === null) {
|
||||
|
|
@ -172,14 +199,14 @@ export class IdleTimeHandler {
|
|||
}
|
||||
} catch (err) {
|
||||
error = err;
|
||||
this.logError('GNOME DBus error', err);
|
||||
this._logError('GNOME DBus error', err);
|
||||
idleTime = 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'xprintidle':
|
||||
try {
|
||||
const result = await this.getXprintidleTime();
|
||||
const result = await this._getXprintidleTime();
|
||||
idleTime = result ?? 0;
|
||||
log.debug(`xprintidle idle time: ${idleTime}ms`);
|
||||
if (result === null) {
|
||||
|
|
@ -187,14 +214,14 @@ export class IdleTimeHandler {
|
|||
}
|
||||
} catch (err) {
|
||||
error = err;
|
||||
this.logError('xprintidle error', err);
|
||||
this._logError('xprintidle error', err);
|
||||
idleTime = 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'loginctl':
|
||||
try {
|
||||
const result = await this.getLoginctlIdleTime();
|
||||
const result = await this._getLoginctlIdleTime();
|
||||
idleTime = result ?? 0;
|
||||
log.debug(`loginctl idle time: ${idleTime}ms`);
|
||||
if (result === null) {
|
||||
|
|
@ -202,7 +229,7 @@ export class IdleTimeHandler {
|
|||
}
|
||||
} catch (err) {
|
||||
error = err;
|
||||
this.logError('loginctl error', err);
|
||||
this._logError('loginctl error', err);
|
||||
idleTime = 0;
|
||||
}
|
||||
break;
|
||||
|
|
@ -214,7 +241,6 @@ export class IdleTimeHandler {
|
|||
break;
|
||||
}
|
||||
|
||||
// Always log the final result for debugging
|
||||
log.info(
|
||||
`Idle detection result: ${idleTime}ms (method: ${methodUsed}, hasError: ${!!error})`,
|
||||
);
|
||||
|
|
@ -222,18 +248,16 @@ export class IdleTimeHandler {
|
|||
return idleTime;
|
||||
}
|
||||
|
||||
private async getGnomeIdleTime(): Promise<number | null> {
|
||||
private async _getGnomeIdleTime(): Promise<number | null> {
|
||||
try {
|
||||
const isSnap = !!process.env.SNAP;
|
||||
const isSnap = this._environment.isSnap;
|
||||
if (isSnap) {
|
||||
log.debug('Attempting GNOME idle detection inside snap environment');
|
||||
}
|
||||
|
||||
// Try gdbus first as it might work better in snap environments
|
||||
let command =
|
||||
'gdbus call --session --dest org.gnome.Mutter.IdleMonitor --object-path /org/gnome/Mutter/IdleMonitor/Core --method org.gnome.Mutter.IdleMonitor.GetIdletime';
|
||||
|
||||
// Check if gdbus is available
|
||||
try {
|
||||
await execAsync('which gdbus', { timeout: 1000 });
|
||||
} catch {
|
||||
|
|
@ -243,7 +267,6 @@ export class IdleTimeHandler {
|
|||
);
|
||||
return null;
|
||||
}
|
||||
// Fall back to dbus-send if gdbus is not available outside of snap
|
||||
command =
|
||||
'dbus-send --print-reply --dest=org.gnome.Mutter.IdleMonitor /org/gnome/Mutter/IdleMonitor/Core org.gnome.Mutter.IdleMonitor.GetIdletime';
|
||||
}
|
||||
|
|
@ -260,41 +283,34 @@ export class IdleTimeHandler {
|
|||
throw error;
|
||||
}
|
||||
|
||||
// Parse the response - gdbus format: (uint64 1234567890,)
|
||||
// dbus-send format: uint64 1234567890
|
||||
const match = stdout.match(/uint64\s+(\d+)|(?:\(uint64\s+)?(\d+)(?:,\))?/);
|
||||
if (match) {
|
||||
const idleMs = parseInt(match[1] || match[2], 10);
|
||||
// Validate the result is reasonable (not negative, not extremely large)
|
||||
if (idleMs >= 0 && idleMs < Number.MAX_SAFE_INTEGER) {
|
||||
return idleMs;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// DBus call failed, likely not running GNOME or DBus not available
|
||||
throw error;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// Alternative approach: Use loginctl for systemd-based systems
|
||||
private async getLoginctlIdleTime(): Promise<number | null> {
|
||||
private async _getLoginctlIdleTime(): Promise<number | null> {
|
||||
try {
|
||||
const { stdout } = await execAsync('loginctl show-session -p IdleSinceHint', {
|
||||
timeout: 3000,
|
||||
});
|
||||
|
||||
// Parse the output to get idle time
|
||||
const match = stdout.match(/IdleSinceHint=(\d+)/);
|
||||
if (match && match[1]) {
|
||||
const idleSince = parseInt(match[1], 10);
|
||||
// IdleSinceHint is in microseconds since epoch, 0 means active
|
||||
if (idleSince > 0) {
|
||||
const nowMs = Date.now() * 1000;
|
||||
const idleMs = nowMs - idleSince; // Convert to milliseconds
|
||||
const idleMs = nowMs - idleSince;
|
||||
if (idleMs >= 0 && idleMs < Number.MAX_SAFE_INTEGER) {
|
||||
return Math.floor(idleMs / 1000); // Convert back to milliseconds
|
||||
return Math.floor(idleMs / 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -305,8 +321,7 @@ export class IdleTimeHandler {
|
|||
return null;
|
||||
}
|
||||
|
||||
// Check if we can use xprintidle as a fallback (works on some Wayland systems with XWayland)
|
||||
private async getXprintidleTime(): Promise<number | null> {
|
||||
private async _getXprintidleTime(): Promise<number | null> {
|
||||
try {
|
||||
const { stdout } = await execAsync('xprintidle', { timeout: 3000 });
|
||||
const idleMs = parseInt(stdout.trim(), 10);
|
||||
|
|
@ -320,21 +335,19 @@ export class IdleTimeHandler {
|
|||
return null;
|
||||
}
|
||||
|
||||
// Keep the old method for compatibility but use the new approach
|
||||
async getIdleTimeWithFallbacks(): Promise<number> {
|
||||
return this.getIdleTime();
|
||||
}
|
||||
|
||||
// Expose working method for debugging
|
||||
get currentMethod(): IdleDetectionMethod {
|
||||
return this.workingMethod;
|
||||
return this._workingMethod;
|
||||
}
|
||||
|
||||
private logError(context: string, error: unknown): void {
|
||||
private _logError(context: string, error: unknown): void {
|
||||
const now = Date.now();
|
||||
if (now - this.lastErrorLog > this.ERROR_LOG_INTERVAL) {
|
||||
if (now - this._lastErrorLog > ERROR_LOG_INTERVAL) {
|
||||
log.debug(`${context} (falling back to 0):`, error);
|
||||
this.lastErrorLog = now;
|
||||
this._lastErrorLog = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue