From 1f73aeee516744da031423a9c6f38d270b2b80ef Mon Sep 17 00:00:00 2001 From: ferreo Date: Tue, 14 Jan 2025 13:38:23 +0000 Subject: [PATCH] Add config and profile autowatching + config for proton system files --- falcond/debian/changelog | 6 + falcond/src/{ => clients}/dbus.zig | 1 - falcond/src/{ => clients}/power_profiles.zig | 10 +- falcond/src/{ => clients}/scx_scheds.zig | 2 +- falcond/src/{ => clients}/vcache_setting.zig | 8 +- falcond/src/config.zig | 51 ---- falcond/src/config/config.zig | 92 +++++++ falcond/src/{ => config}/confloader.zig | 0 falcond/src/{ => config}/parser.zig | 40 +++ falcond/src/daemon.zig | 235 ++++++++++------ falcond/src/main.zig | 14 +- falcond/src/profile.zig | 270 ------------------- falcond/src/profile/loader.zig | 37 +++ falcond/src/profile/manager.zig | 103 +++++++ falcond/src/profile/matcher.zig | 110 ++++++++ falcond/src/profile/types.zig | 25 ++ main.sh | 2 +- 17 files changed, 580 insertions(+), 426 deletions(-) rename falcond/src/{ => clients}/dbus.zig (99%) rename falcond/src/{ => clients}/power_profiles.zig (92%) rename falcond/src/{ => clients}/scx_scheds.zig (99%) rename falcond/src/{ => clients}/vcache_setting.zig (84%) delete mode 100644 falcond/src/config.zig create mode 100644 falcond/src/config/config.zig rename falcond/src/{ => config}/confloader.zig (100%) rename falcond/src/{ => config}/parser.zig (91%) delete mode 100644 falcond/src/profile.zig create mode 100644 falcond/src/profile/loader.zig create mode 100644 falcond/src/profile/manager.zig create mode 100644 falcond/src/profile/matcher.zig create mode 100644 falcond/src/profile/types.zig diff --git a/falcond/debian/changelog b/falcond/debian/changelog index 520a20b..43c46a7 100644 --- a/falcond/debian/changelog +++ b/falcond/debian/changelog @@ -1,3 +1,9 @@ +falcond (1.0.1-101pika3) pika; urgency=low + + * Add config and profile autowatching + config for proton system files + + -- ferreo Sun, 12 Jan 2025 13:48:00 +0300 + falcond (1.0.0-101pika3) pika; urgency=low * Initial release diff --git a/falcond/src/dbus.zig b/falcond/src/clients/dbus.zig similarity index 99% rename from falcond/src/dbus.zig rename to falcond/src/clients/dbus.zig index 4554ddf..226f0e0 100644 --- a/falcond/src/dbus.zig +++ b/falcond/src/clients/dbus.zig @@ -34,7 +34,6 @@ pub const DBusError = error{ NetworkSubsystemFailed, }; -/// Simple DBus interface that uses busctl under the hood pub const DBus = struct { allocator: std.mem.Allocator, bus_name: []const u8, diff --git a/falcond/src/power_profiles.zig b/falcond/src/clients/power_profiles.zig similarity index 92% rename from falcond/src/power_profiles.zig rename to falcond/src/clients/power_profiles.zig index c64f532..3c0ca86 100644 --- a/falcond/src/power_profiles.zig +++ b/falcond/src/clients/power_profiles.zig @@ -1,6 +1,6 @@ const std = @import("std"); -const dbus = @import("dbus.zig"); -const Config = @import("config.zig").Config; +const dbus = @import("./dbus.zig"); +const Config = @import("../config/config.zig").Config; pub const PowerProfiles = struct { const PP_NAME = "org.freedesktop.UPower.PowerProfiles"; @@ -9,11 +9,11 @@ pub const PowerProfiles = struct { allocator: std.mem.Allocator, dbus: dbus.DBus, - config: *Config, + config: Config, original_profile: ?[]const u8, has_performance: bool, - pub fn init(allocator: std.mem.Allocator, config: *Config) !*PowerProfiles { + pub fn init(allocator: std.mem.Allocator, config: Config) !*PowerProfiles { var self = try allocator.create(PowerProfiles); errdefer allocator.destroy(self); @@ -107,6 +107,7 @@ pub const PowerProfiles = struct { if (self.dbus.getProperty("ActiveProfile")) |profile| { defer self.allocator.free(profile); if (!std.mem.eql(u8, profile, "performance")) { + std.log.info("Storing original power profile: {s}", .{profile}); self.original_profile = self.allocator.dupe(u8, profile) catch |err| { std.log.err("Failed to store original power profile: {}", .{err}); return; @@ -122,6 +123,7 @@ pub const PowerProfiles = struct { pub fn disablePerformanceMode(self: *PowerProfiles) void { if (self.original_profile) |profile| { + std.log.info("Restoring power profile to: {s}", .{profile}); self.dbus.setProperty("ActiveProfile", profile) catch |err| { std.log.err("Failed to restore power profile: {}", .{err}); }; diff --git a/falcond/src/scx_scheds.zig b/falcond/src/clients/scx_scheds.zig similarity index 99% rename from falcond/src/scx_scheds.zig rename to falcond/src/clients/scx_scheds.zig index 76139f6..bc7296b 100644 --- a/falcond/src/scx_scheds.zig +++ b/falcond/src/clients/scx_scheds.zig @@ -1,5 +1,5 @@ const std = @import("std"); -const dbus = @import("dbus.zig"); +const dbus = @import("./dbus.zig"); pub const ScxError = dbus.DBusError; diff --git a/falcond/src/vcache_setting.zig b/falcond/src/clients/vcache_setting.zig similarity index 84% rename from falcond/src/vcache_setting.zig rename to falcond/src/clients/vcache_setting.zig index 3698668..c361975 100644 --- a/falcond/src/vcache_setting.zig +++ b/falcond/src/clients/vcache_setting.zig @@ -14,7 +14,10 @@ var previous_mode_buffer: [10]u8 = undefined; pub fn applyVCacheMode(vcache_mode: VCacheMode) void { const file = fs.openFileAbsolute(vcache_path, .{ .mode = .read_write }) catch |err| switch (err) { - error.FileNotFound => return, + error.FileNotFound => { + std.log.info("AMD 3D vcache support not detected", .{}); + return; + }, else => { std.log.err("Failed to open vcache file: {}", .{err}); return; @@ -24,6 +27,7 @@ pub fn applyVCacheMode(vcache_mode: VCacheMode) void { if (vcache_mode == .none) { if (previous_mode) |mode| { + std.log.info("Restoring previous vcache mode: {s}", .{mode}); file.writeAll(mode) catch |err| { std.log.err("Failed to restore previous vcache mode: {}", .{err}); }; @@ -50,6 +54,8 @@ pub fn applyVCacheMode(vcache_mode: VCacheMode) void { .cache => "cache", .none => unreachable, }; + + std.log.info("Setting vcache mode to: {s}", .{mode_str}); file.writeAll(mode_str) catch |err| { std.log.err("Failed to write vcache mode: {}", .{err}); }; diff --git a/falcond/src/config.zig b/falcond/src/config.zig deleted file mode 100644 index 3c352df..0000000 --- a/falcond/src/config.zig +++ /dev/null @@ -1,51 +0,0 @@ -const std = @import("std"); -const fs = std.fs; -const confloader = @import("confloader.zig"); -const vcache_setting = @import("vcache_setting.zig"); -const scx_scheds = @import("scx_scheds.zig"); - -pub const Config = struct { - enable_performance_mode: bool = true, - scx_sched: scx_scheds.ScxScheduler = .none, - scx_sched_props: ?scx_scheds.ScxSchedModes = null, - vcache_mode: vcache_setting.VCacheMode = .none, - - pub fn load(allocator: std.mem.Allocator) !Config { - const config_path = "/etc/falcond/config.conf"; - var config = Config{}; - const file = fs.openFileAbsolute(config_path, .{}) catch |err| switch (err) { - error.FileNotFound => { - try config.save(); - return config; - }, - else => return err, - }; - defer file.close(); - - config = try confloader.loadConf(Config, allocator, config_path); - return config; - } - - pub fn save(self: Config) !void { - const config_dir = "/etc/falcond/"; - - fs.makeDirAbsolute(config_dir) catch |err| switch (err) { - error.PathAlreadyExists => {}, - else => return err, - }; - - var file_buf: [fs.max_path_bytes]u8 = undefined; - const config_path = try std.fmt.bufPrint( - &file_buf, - "{s}/config.conf", - .{config_dir}, - ); - - const file = try fs.createFileAbsolute(config_path, .{}); - defer file.close(); - - try file.writer().print("enable_performance_mode = {}\n", .{self.enable_performance_mode}); - try file.writer().print("scx_sched = {s}\n", .{@tagName(self.scx_sched)}); - try file.writer().print("vcache_mode = {s}\n", .{@tagName(self.vcache_mode)}); - } -}; diff --git a/falcond/src/config/config.zig b/falcond/src/config/config.zig new file mode 100644 index 0000000..f20c564 --- /dev/null +++ b/falcond/src/config/config.zig @@ -0,0 +1,92 @@ +const std = @import("std"); +const fs = std.fs; +const confloader = @import("confloader.zig"); +const vcache_setting = @import("../clients/vcache_setting.zig"); +const scx_scheds = @import("../clients/scx_scheds.zig"); + +const default_system_processes = [_][]const u8{ + "steam.exe", + "services.exe", + "winedevice.exe", + "plugplay.exe", + "svchost.exe", + "explorer.exe", + "rpcss.exe", + "tabtip.exe", + "wineboot.exe", + "rundll32.exe", + "iexplore.exe", + "conhost.exe", + "crashpad_handler.exe", + "iscriptevaluator.exe", + "VC_redist.x86.exe", + "VC_redist.x64.exe", + "cmd.exe", + "REDEngineErrorReporter.exe", + "REDprelauncher.exe", + "SteamService.exe", + "UnityCrashHandler64.exe", + "start.exe", +}; + +pub const Config = struct { + enable_performance_mode: bool = true, + scx_sched: scx_scheds.ScxScheduler = .none, + scx_sched_props: ?scx_scheds.ScxSchedModes = null, + vcache_mode: vcache_setting.VCacheMode = .none, + system_processes: []const []const u8 = &default_system_processes, + arena: ?std.heap.ArenaAllocator = null, + + pub fn load(allocator: std.mem.Allocator, path: []const u8) !Config { + var config = Config{}; + const file = fs.openFileAbsolute(path, .{}) catch |err| switch (err) { + error.FileNotFound => { + try config.save(path); + return config; + }, + else => return err, + }; + defer file.close(); + + var arena = std.heap.ArenaAllocator.init(allocator); + errdefer arena.deinit(); + + config = try confloader.loadConf(Config, arena.allocator(), path); + config.arena = arena; + return config; + } + + pub fn deinit(self: *Config) void { + if (self.arena) |*arena| { + arena.deinit(); + self.arena = null; + } + } + + pub fn save(self: Config, path: []const u8) !void { + var file_buf: [fs.max_path_bytes]u8 = undefined; + const config_dir = try std.fmt.bufPrint( + &file_buf, + "{s}/", + .{std.fs.path.dirname(path).?}, + ); + + fs.makeDirAbsolute(config_dir) catch |err| switch (err) { + error.PathAlreadyExists => {}, + else => return err, + }; + + const file = try fs.createFileAbsolute(path, .{}); + defer file.close(); + + try file.writer().print("enable_performance_mode = {}\n", .{self.enable_performance_mode}); + try file.writer().print("scx_sched = {s}\n", .{@tagName(self.scx_sched)}); + try file.writer().print("vcache_mode = {s}\n", .{@tagName(self.vcache_mode)}); + + try file.writer().print("system_processes = [\n", .{}); + for (self.system_processes) |proc| { + try file.writer().print(" \"{s}\",\n", .{proc}); + } + try file.writer().print("]\n", .{}); + } +}; diff --git a/falcond/src/confloader.zig b/falcond/src/config/confloader.zig similarity index 100% rename from falcond/src/confloader.zig rename to falcond/src/config/confloader.zig diff --git a/falcond/src/parser.zig b/falcond/src/config/parser.zig similarity index 91% rename from falcond/src/parser.zig rename to falcond/src/config/parser.zig index c2a58ce..0ce3cd6 100644 --- a/falcond/src/parser.zig +++ b/falcond/src/config/parser.zig @@ -142,6 +142,43 @@ pub fn Parser(comptime T: type) type { return error.InvalidSyntax; } + fn parseStringArray(self: *Self) ![]const []const u8 { + if (self.pos >= self.content.len or self.content[self.pos] != '[') + return error.InvalidSyntax; + + self.pos += 1; + var values = std.ArrayList([]const u8).init(self.allocator); + errdefer { + for (values.items) |str| { + self.allocator.free(str); + } + values.deinit(); + } + + while (self.pos < self.content.len) { + self.skipWhitespace(); + if (self.content[self.pos] == ']') { + self.pos += 1; + return values.toOwnedSlice(); + } + + const str = try self.parseString(); + try values.append(str); + + self.skipWhitespace(); + if (self.content[self.pos] == ',') { + self.pos += 1; + continue; + } + if (self.content[self.pos] == ']') { + self.pos += 1; + return values.toOwnedSlice(); + } + return error.InvalidSyntax; + } + return error.InvalidSyntax; + } + fn parseIdentifier(self: *Self) ![]const u8 { const start = self.pos; const v_size = std.simd.suggestVectorLength(u8) orelse 32; @@ -259,6 +296,9 @@ pub fn Parser(comptime T: type) type { i64 => { @field(result, field.name) = try self.parseArray(); }, + []const u8 => { + @field(result, field.name) = try self.parseStringArray(); + }, else => return error.InvalidSyntax, } }, diff --git a/falcond/src/daemon.zig b/falcond/src/daemon.zig index 8710d63..6f92a80 100644 --- a/falcond/src/daemon.zig +++ b/falcond/src/daemon.zig @@ -1,34 +1,150 @@ const std = @import("std"); -const ProfileManager = @import("profile.zig").ProfileManager; -const Profile = @import("profile.zig").Profile; -const Config = @import("config.zig").Config; +const ProfileManager = @import("profile/manager.zig").ProfileManager; +const Profile = @import("profile/types.zig").Profile; +const ProfileLoader = @import("profile/loader.zig"); +const Config = @import("config/config.zig").Config; const linux = std.os.linux; const posix = std.posix; -const PowerProfiles = @import("power_profiles.zig").PowerProfiles; -const scx_scheds = @import("scx_scheds.zig"); +const PowerProfiles = @import("clients/power_profiles.zig").PowerProfiles; +const scx_scheds = @import("clients/scx_scheds.zig"); pub const Daemon = struct { allocator: std.mem.Allocator, + config_path: []const u8, profile_manager: ProfileManager, oneshot: bool, known_pids: ?std.AutoHashMap(u32, *const Profile), power_profiles: *PowerProfiles, + performance_mode: bool, + last_profiles_check: i128, + last_config_check: i128, + config: Config, const Self = @This(); - pub fn init(allocator: std.mem.Allocator, config: ?*Config, oneshot: bool, power_profiles: *PowerProfiles) !Self { - var profile_manager = ProfileManager.init(allocator, power_profiles, config.?); - try profile_manager.loadProfiles(oneshot); + pub fn init(allocator: std.mem.Allocator, config_path: []const u8, oneshot: bool) !*Self { + const config_path_owned = try allocator.dupe(u8, config_path); + errdefer allocator.free(config_path_owned); + const config = try Config.load(allocator, config_path); + const power_profiles = try PowerProfiles.init(allocator, config); + errdefer power_profiles.deinit(); + + const performance_mode = power_profiles.isPerformanceAvailable(); + if (performance_mode) { + std.log.info("Performance profile available - power profile management enabled", .{}); + } + + var profile_manager = ProfileManager.init(allocator, power_profiles, config); + try ProfileLoader.loadProfiles(allocator, &profile_manager.profiles, &profile_manager.proton_profile, oneshot); + + const current_time = std.time.nanoTimestamp(); try scx_scheds.init(allocator); - return Self{ + const self = try allocator.create(Self); + self.* = .{ .allocator = allocator, .profile_manager = profile_manager, .oneshot = oneshot, - .known_pids = null, + .known_pids = if (!oneshot) std.AutoHashMap(u32, *const Profile).init(allocator) else null, .power_profiles = power_profiles, + .last_profiles_check = current_time, + .last_config_check = current_time, + .config = config, + .config_path = config_path_owned, + .performance_mode = performance_mode, }; + + return self; + } + + pub fn deinit(self: *Self) void { + scx_scheds.deinit(); + self.profile_manager.deinit(); + self.power_profiles.deinit(); + if (self.known_pids) |*map| { + map.deinit(); + } + self.config.deinit(); + self.allocator.free(self.config_path); + self.allocator.destroy(self); + } + + fn reloadConfig(self: *Self) !void { + if (self.known_pids) |*map| { + map.clearRetainingCapacity(); + } + + const config = try Config.load(self.allocator, self.config_path); + const power_profiles = try PowerProfiles.init(self.allocator, config); + self.power_profiles.deinit(); + self.config.deinit(); + self.config = config; + self.power_profiles = power_profiles; + self.performance_mode = self.power_profiles.isPerformanceAvailable(); + + var profile_manager = ProfileManager.init(self.allocator, power_profiles, config); + try ProfileLoader.loadProfiles(self.allocator, &profile_manager.profiles, &profile_manager.proton_profile, self.oneshot); + self.profile_manager.deinit(); + self.profile_manager = profile_manager; + } + + fn checkConfigChanges(self: *Self) !bool { + const stat = try std.fs.cwd().statFile(self.config_path); + const mtime = @as(i128, @intCast(stat.mtime)); + if (mtime > self.last_config_check) { + std.log.info("Config file changed, reloading profiles", .{}); + self.last_config_check = std.time.nanoTimestamp(); + return true; + } + return false; + } + + fn checkProfilesChanges(self: *Self) !bool { + var dir = try std.fs.cwd().openDir(self.profile_manager.profiles_dir, .{ .iterate = true }); + defer dir.close(); + + var latest_mtime: i128 = self.last_profiles_check; + var file_count: usize = 0; + var iter = dir.iterate(); + while (try iter.next()) |entry| { + if (entry.kind == .file) { + file_count += 1; + const stat = try dir.statFile(entry.name); + const mtime = @as(i128, @intCast(stat.mtime)); + if (mtime > latest_mtime) { + latest_mtime = mtime; + } + } + } + + if (latest_mtime > self.last_profiles_check or file_count != self.profile_manager.profiles.items.len) { + std.log.info("Profile changes detected, reloading profiles", .{}); + self.last_profiles_check = std.time.nanoTimestamp(); + return true; + } + + return false; + } + + pub fn run(self: *Self) !void { + if (self.oneshot) { + try self.handleProcesses(); + return; + } + + while (true) { + const config_changed = try self.checkConfigChanges(); + const profiles_changed = try self.checkProfilesChanges(); + + if (config_changed or profiles_changed) { + try self.reloadConfig(); + } + + try self.handleProcesses(); + + std.time.sleep(std.time.ns_per_s * 9); + } } fn scanProcesses(allocator: std.mem.Allocator) !std.AutoHashMap(u32, []const u8) { @@ -91,7 +207,7 @@ pub const Daemon = struct { return try allocator.dupe(u8, exe_name); } - pub fn checkProcesses(self: *Self) !void { + fn handleProcesses(self: *Self) !void { var arena = std.heap.ArenaAllocator.init(self.allocator); defer arena.deinit(); const arena_allocator = arena.allocator(); @@ -113,14 +229,16 @@ pub const Daemon = struct { if (!self.oneshot) { if (self.known_pids) |*known| { if (!known.contains(pid)) { - if (try self.profile_manager.matchProcess(arena.allocator(), try std.fmt.allocPrint(arena_allocator, "{d}", .{pid}), process_name)) |profile| { + if (try self.profile_manager.matchProcess(arena_allocator, try std.fmt.allocPrint(arena_allocator, "{d}", .{pid}), process_name)) |profile| { try known.put(pid, profile); try self.profile_manager.activateProfile(profile); } } } } else { - try self.handleProcess(try std.fmt.allocPrint(arena_allocator, "{d}", .{pid}), process_name); + if (try self.profile_manager.matchProcess(arena_allocator, try std.fmt.allocPrint(arena_allocator, "{d}", .{pid}), process_name)) |profile| { + try self.profile_manager.activateProfile(profile); + } } } @@ -130,81 +248,30 @@ pub const Daemon = struct { while (known_it.next()) |entry| { const pid = entry.key_ptr.*; if (!processes.contains(pid)) { - try self.handleProcessExit(try std.fmt.allocPrint(arena_allocator, "{d}", .{pid})); - } - } - } - } - } + const profile = entry.value_ptr.*; + var found_profile = false; - pub fn run(self: *Self) !void { - if (!self.oneshot) { - self.known_pids = std.AutoHashMap(u32, *const Profile).init(self.allocator); - } - - try self.checkProcesses(); - - if (self.oneshot) { - return; - } - - while (true) { - try self.checkProcesses(); - std.time.sleep(std.time.ns_per_s * 3); - } - } - - pub fn deinit(self: *Self) void { - scx_scheds.deinit(); - if (self.known_pids) |*pids| { - pids.deinit(); - } - self.profile_manager.deinit(); - } - - pub fn handleProcess(self: *Self, pid: []const u8, process_name: []const u8) !void { - var arena = std.heap.ArenaAllocator.init(self.allocator); - defer arena.deinit(); - - if (try self.profile_manager.matchProcess(arena.allocator(), pid, process_name)) |profile| { - if (!self.oneshot) { - if (self.known_pids) |*known| { - try known.put(try std.fmt.parseInt(u32, pid, 10), profile); - } - } - try self.profile_manager.activateProfile(profile); - } - } - - pub fn handleProcessExit(self: *Self, pid: []const u8) !void { - if (self.known_pids) |*pids| { - const pid_num = std.fmt.parseInt(u32, pid, 10) catch |err| { - std.log.warn("Failed to parse PID: {}", .{err}); - return; - }; - - if (pids.get(pid_num)) |profile| { - var found_profile = false; - - if (self.profile_manager.active_profile) |active| { - if (active == profile) { - std.log.info("Process {s} has terminated", .{profile.name}); - try self.profile_manager.deactivateProfile(active); - found_profile = true; - } - } - - if (!found_profile) { - for (self.profile_manager.queued_profiles.items, 0..) |queued, i| { - if (queued == profile) { - std.log.info("Process {s} has terminated", .{profile.name}); - _ = self.profile_manager.queued_profiles.orderedRemove(i); - break; + if (self.profile_manager.active_profile) |active| { + if (active == profile) { + std.log.info("Process {s} has terminated", .{profile.name}); + try self.profile_manager.deactivateProfile(active); + found_profile = true; + } } + + if (!found_profile) { + for (self.profile_manager.queued_profiles.items, 0..) |queued, i| { + if (queued == profile) { + std.log.info("Process {s} has terminated", .{profile.name}); + _ = self.profile_manager.queued_profiles.orderedRemove(i); + break; + } + } + } + + _ = known.remove(pid); } } - - _ = pids.remove(pid_num); } } } diff --git a/falcond/src/main.zig b/falcond/src/main.zig index 237e49a..e60b15f 100644 --- a/falcond/src/main.zig +++ b/falcond/src/main.zig @@ -1,7 +1,5 @@ const std = @import("std"); const Daemon = @import("daemon.zig").Daemon; -const Config = @import("config.zig").Config; -const PowerProfiles = @import("power_profiles.zig").PowerProfiles; pub const std_options = std.Options{ .log_level = .debug, @@ -80,16 +78,6 @@ pub fn main() !void { }, }; - var config = try Config.load(allocator); - var power_profiles = try PowerProfiles.init(allocator, &config); - defer power_profiles.deinit(); - - if (!power_profiles.isPerformanceAvailable()) { - std.log.warn("Performance profile not available - power profile management disabled", .{}); - } else { - std.log.info("Performance profile available - power profile management enabled", .{}); - } - const args = try std.process.argsAlloc(allocator); defer std.process.argsFree(allocator, args); @@ -97,7 +85,7 @@ pub fn main() !void { if (std.mem.eql(u8, arg, "--oneshot")) break true; } else false; - var daemon = try Daemon.init(allocator, &config, oneshot, power_profiles); + var daemon = try Daemon.init(allocator, "/etc/falcond/config.conf", oneshot); defer daemon.deinit(); try daemon.run(); diff --git a/falcond/src/profile.zig b/falcond/src/profile.zig deleted file mode 100644 index 29e3019..0000000 --- a/falcond/src/profile.zig +++ /dev/null @@ -1,270 +0,0 @@ -const std = @import("std"); -const fs = std.fs; -const confloader = @import("confloader.zig"); -const PowerProfiles = @import("power_profiles.zig").PowerProfiles; -const Config = @import("config.zig").Config; -const vcache_setting = @import("vcache_setting.zig"); -const scx_scheds = @import("scx_scheds.zig"); -const Child = std.process.Child; -const linux = std.os.linux; -const CPU_SETSIZE = 1024; -const CPU_SET = extern struct { - bits: [CPU_SETSIZE / 64]u64, -}; - -pub const Profile = struct { - const LscpuCoreStrategy = enum { HighestFreq, Sequential }; - - name: []const u8, - performance_mode: bool = false, - scx_sched: scx_scheds.ScxScheduler = .none, - scx_sched_props: ?scx_scheds.ScxSchedModes = null, - vcache_mode: vcache_setting.VCacheMode = .cache, - - pub fn matches(self: *const Profile, process_name: []const u8) bool { - const is_match = std.ascii.eqlIgnoreCase(self.name, process_name); - if (is_match) { - std.log.info("Found match: {s} for process {s}", .{ self.name, process_name }); - } - return is_match; - } -}; - -const CacheEntry = struct { - pid: u32, - timestamp: i64, - is_proton: bool, -}; - -pub const ProfileManager = struct { - allocator: std.mem.Allocator, - profiles: std.ArrayList(Profile), - proton_profile: ?*const Profile, - active_profile: ?*const Profile = null, - queued_profiles: std.ArrayList(*const Profile), - power_profiles: *PowerProfiles, - config: *const Config, - - // Don't match Wine/Proton infrastructure - const system_processes = [_][]const u8{ - "steam.exe", - "services.exe", - "winedevice.exe", - "plugplay.exe", - "svchost.exe", - "explorer.exe", - "rpcss.exe", - "tabtip.exe", - "wineboot.exe", - "rundll32.exe", - "iexplore.exe", - "conhost.exe", - "crashpad_handler.exe", - "iscriptevaluator.exe", - "VC_redist.x86.exe", - "VC_redist.x64.exe", - "cmd.exe", - "REDEngineErrorReporter.exe", - "REDprelauncher.exe", - "SteamService.exe", - "UnityCrashHandler64.exe", - "start.exe", - }; - - pub fn init(allocator: std.mem.Allocator, power_profiles: *PowerProfiles, config: *const Config) ProfileManager { - return .{ - .allocator = allocator, - .profiles = std.ArrayList(Profile).init(allocator), - .proton_profile = null, - .queued_profiles = std.ArrayList(*const Profile).init(allocator), - .power_profiles = power_profiles, - .config = config, - }; - } - - pub fn activateProfile(self: *ProfileManager, profile: *const Profile) !void { - if (self.active_profile == null) { - std.log.info("Activating profile: {s}", .{profile.name}); - self.active_profile = profile; - - if (profile.performance_mode and self.power_profiles.isPerformanceAvailable()) { - std.log.info("Enabling performance mode for profile: {s}", .{profile.name}); - self.power_profiles.enablePerformanceMode(); - } - - const effective_mode = if (self.config.vcache_mode != .none) - self.config.vcache_mode - else - profile.vcache_mode; - vcache_setting.applyVCacheMode(effective_mode); - - const effective_sched = if (self.config.scx_sched != .none) - self.config.scx_sched - else - profile.scx_sched; - - const effective_scx_mode = if (self.config.scx_sched != .none) - self.config.scx_sched_props - else - profile.scx_sched_props; - - scx_scheds.applyScheduler(self.allocator, effective_sched, effective_scx_mode); - } else { - std.log.info("Queueing profile: {s} (active: {s})", .{ profile.name, self.active_profile.?.name }); - try self.queued_profiles.append(profile); - } - } - - pub fn deactivateProfile(self: *ProfileManager, profile: *const Profile) !void { - if (self.active_profile == profile) { - std.log.info("Deactivating profile: {s}", .{profile.name}); - self.active_profile = null; - - if (profile.performance_mode) { - std.log.info("Disabling performance mode for profile: {s}", .{profile.name}); - self.power_profiles.disablePerformanceMode(); - } - - vcache_setting.applyVCacheMode(.none); - scx_scheds.restorePreviousState(self.allocator); - if (self.queued_profiles.items.len > 0) { - const next_profile = self.queued_profiles.orderedRemove(0); - std.log.info("Activating next queued profile: {s}", .{next_profile.name}); - try self.activateProfile(next_profile); - } - } else { - for (self.queued_profiles.items, 0..) |queued, i| { - if (queued == profile) { - std.log.info("Removing queued profile: {s}", .{profile.name}); - _ = self.queued_profiles.orderedRemove(i); - break; - } - } - } - } - - pub fn loadProfiles(self: *ProfileManager, oneshot: bool) !void { - if (oneshot) { - try self.profiles.append(Profile{ - .name = try self.allocator.dupe(u8, "Hades3.exe"), - }); - std.log.info("Loaded oneshot profile: Hades3.exe", .{}); - - try self.profiles.append(Profile{ - .name = try self.allocator.dupe(u8, "Proton"), - }); - std.log.info("Loaded oneshot profile: Proton", .{}); - - self.proton_profile = &self.profiles.items[1]; - } else { - var profiles = try confloader.loadConfDir(Profile, self.allocator, "/usr/share/falcond/profiles"); - defer profiles.deinit(); - - try self.profiles.appendSlice(profiles.items); - - for (self.profiles.items) |*profile| { - if (std.mem.eql(u8, profile.name, "Proton")) { - self.proton_profile = profile; - std.log.info("Found Proton profile: {s}", .{profile.name}); - break; - } - } - - std.log.info("Loaded {d} profiles", .{self.profiles.items.len}); - } - } - - fn isProtonParent(_: *const ProfileManager, arena: std.mem.Allocator, pid: []const u8) !bool { - var path_buf: [std.fs.max_path_bytes]u8 = undefined; - const status_path = try std.fmt.bufPrint(&path_buf, "/proc/{s}/status", .{pid}); - - const file = std.fs.openFileAbsolute(status_path, .{}) catch |err| { - std.log.debug("Failed to open {s}: {}", .{ status_path, err }); - return switch (err) { - error.AccessDenied, error.FileNotFound => false, - else => err, - }; - }; - defer file.close(); - - const content = try file.readToEndAlloc(arena, std.math.maxInt(usize)); - - const ppid_line = std.mem.indexOf(u8, content, "PPid:") orelse return false; - const line_end = std.mem.indexOfScalarPos(u8, content, ppid_line, '\n') orelse content.len; - const ppid_start = ppid_line + 5; // Length of "PPid:" - const ppid = std.mem.trim(u8, content[ppid_start..line_end], " \t"); - - const parent_cmdline_path = try std.fmt.bufPrint(&path_buf, "/proc/{s}/cmdline", .{ppid}); - const parent_file = std.fs.openFileAbsolute(parent_cmdline_path, .{}) catch |err| { - std.log.debug("Failed to open parent cmdline {s}: {}", .{ parent_cmdline_path, err }); - return switch (err) { - error.AccessDenied, error.FileNotFound => false, - else => err, - }; - }; - defer parent_file.close(); - - const parent_content = try parent_file.readToEndAlloc(arena, std.math.maxInt(usize)); - return std.mem.indexOf(u8, parent_content, "proton") != null; - } - - fn isProtonGame(self: *ProfileManager, arena: std.mem.Allocator, pid: []const u8, process_name: []const u8) !bool { - if (!std.mem.endsWith(u8, process_name, ".exe")) return false; - - for (system_processes) |sys_proc| { - if (std.mem.eql(u8, process_name, sys_proc)) { - return false; - } - } - - return try self.isProtonParent(arena, pid); - } - - pub fn matchProcess(self: *ProfileManager, arena: std.mem.Allocator, pid: []const u8, process_name: []const u8) !?*const Profile { - const is_exe = std.mem.endsWith(u8, process_name, ".exe"); - var match: ?*const Profile = null; - - for (self.profiles.items) |*profile| { - const is_match = profile != self.proton_profile and profile.matches(process_name); - if (is_match) { - std.log.info("Matched profile {s} for process {s}", .{ profile.name, process_name }); - match = profile; - break; - } - } - - const should_check_proton = match == null and - is_exe and - self.proton_profile != null; - - if (should_check_proton) { - const is_system = for (system_processes) |sys_proc| { - if (std.mem.eql(u8, process_name, sys_proc)) break true; - } else false; - - if (!is_system) { - const is_proton = try self.isProtonParent(arena, pid); - if (is_proton) { - std.log.info("Found Proton game: {s}", .{process_name}); - match = self.proton_profile; - } - } - } - - return match; - } - - pub fn deinit(self: *ProfileManager) void { - if (self.active_profile) |profile| { - if (profile.performance_mode) { - self.power_profiles.disablePerformanceMode(); - } - } - - for (self.profiles.items) |*profile| { - self.allocator.free(profile.name); - } - self.queued_profiles.deinit(); - self.profiles.deinit(); - } -}; diff --git a/falcond/src/profile/loader.zig b/falcond/src/profile/loader.zig new file mode 100644 index 0000000..e8e0288 --- /dev/null +++ b/falcond/src/profile/loader.zig @@ -0,0 +1,37 @@ +const std = @import("std"); +const confloader = @import("../config/confloader.zig"); +const types = @import("types.zig"); +const Profile = types.Profile; + +pub fn loadProfiles(allocator: std.mem.Allocator, profiles: *std.ArrayList(Profile), proton_profile: *?*const Profile, oneshot: bool) !void { + if (oneshot) { + try profiles.append(Profile{ + .name = try allocator.dupe(u8, "Hades3.exe"), + .scx_sched = .bpfland, + }); + std.log.info("Loaded oneshot profile: Hades3.exe", .{}); + + try profiles.append(Profile{ + .name = try allocator.dupe(u8, "Proton"), + .scx_sched = .none, + }); + std.log.info("Loaded oneshot profile: Proton", .{}); + + proton_profile.* = &profiles.items[1]; + } else { + var loaded_profiles = try confloader.loadConfDir(Profile, allocator, "/usr/share/falcond/profiles"); + defer loaded_profiles.deinit(); + + try profiles.appendSlice(loaded_profiles.items); + + for (profiles.items) |*profile| { + if (std.mem.eql(u8, profile.name, "Proton")) { + proton_profile.* = profile; + std.log.info("Found Proton profile: {s}", .{profile.name}); + break; + } + } + + std.log.info("Loaded {d} profiles", .{profiles.items.len}); + } +} diff --git a/falcond/src/profile/manager.zig b/falcond/src/profile/manager.zig new file mode 100644 index 0000000..828eec1 --- /dev/null +++ b/falcond/src/profile/manager.zig @@ -0,0 +1,103 @@ +const std = @import("std"); +const types = @import("types.zig"); +const matcher = @import("matcher.zig"); +const Profile = types.Profile; +const PowerProfiles = @import("../clients/power_profiles.zig").PowerProfiles; +const Config = @import("../config/config.zig").Config; +const vcache_setting = @import("../clients/vcache_setting.zig"); +const scx_scheds = @import("../clients/scx_scheds.zig"); + +pub const ProfileManager = struct { + comptime profiles_dir: []const u8 = "/usr/share/falcond/profiles", + allocator: std.mem.Allocator, + profiles: std.ArrayList(Profile), + proton_profile: ?*const Profile, + active_profile: ?*const Profile = null, + queued_profiles: std.ArrayList(*const Profile), + power_profiles: *PowerProfiles, + config: Config, + + pub fn init(allocator: std.mem.Allocator, power_profiles: *PowerProfiles, config: Config) ProfileManager { + return .{ + .allocator = allocator, + .profiles = std.ArrayList(Profile).init(allocator), + .proton_profile = null, + .queued_profiles = std.ArrayList(*const Profile).init(allocator), + .power_profiles = power_profiles, + .config = config, + }; + } + + pub fn deinit(self: *ProfileManager) void { + if (self.active_profile) |profile| { + self.deactivateProfile(profile) catch |err| { + std.log.err("Failed to deactivate profile: {}", .{err}); + }; + } + + for (self.profiles.items) |profile| { + self.allocator.free(profile.name); + } + self.profiles.deinit(); + self.queued_profiles.deinit(); + } + + pub fn activateProfile(self: *ProfileManager, profile: *const Profile) !void { + if (self.active_profile == null) { + std.log.info("Activating profile: {s}", .{profile.name}); + self.active_profile = profile; + + if (profile.performance_mode and self.power_profiles.isPerformanceAvailable()) { + std.log.info("Enabling performance mode for profile: {s}", .{profile.name}); + self.power_profiles.enablePerformanceMode(); + } + + const effective_mode = if (self.config.vcache_mode != .none) + self.config.vcache_mode + else + profile.vcache_mode; + vcache_setting.applyVCacheMode(effective_mode); + + const effective_sched = if (self.config.scx_sched == .none) + profile.scx_sched + else + self.config.scx_sched; + + const effective_scx_mode = if (self.config.scx_sched == .none) + profile.scx_sched_props + else + self.config.scx_sched_props; + + scx_scheds.applyScheduler(self.allocator, effective_sched, effective_scx_mode); + } else { + std.log.info("Queueing profile: {s} (active: {s})", .{ profile.name, self.active_profile.?.name }); + try self.queued_profiles.append(profile); + } + } + + pub fn deactivateProfile(self: *ProfileManager, profile: *const Profile) !void { + if (self.active_profile) |active| { + if (active == profile) { + std.log.info("Deactivating profile: {s}", .{profile.name}); + + if (profile.performance_mode and self.power_profiles.isPerformanceAvailable()) { + std.log.info("Disabling performance mode for profile: {s}", .{profile.name}); + self.power_profiles.disablePerformanceMode(); + } + + vcache_setting.applyVCacheMode(.none); + try scx_scheds.deactivateScheduler(self.allocator); + self.active_profile = null; + + if (self.queued_profiles.items.len > 0) { + const next_profile = self.queued_profiles.orderedRemove(0); + try self.activateProfile(next_profile); + } + } + } + } + + pub fn matchProcess(self: *ProfileManager, arena: std.mem.Allocator, pid: []const u8, process_name: []const u8) !?*const Profile { + return matcher.matchProcess(self.profiles.items, self.proton_profile, arena, pid, process_name, self.config); + } +}; diff --git a/falcond/src/profile/matcher.zig b/falcond/src/profile/matcher.zig new file mode 100644 index 0000000..31adf83 --- /dev/null +++ b/falcond/src/profile/matcher.zig @@ -0,0 +1,110 @@ +const std = @import("std"); +const types = @import("types.zig"); +const Profile = types.Profile; +const Config = @import("../config/config.zig").Config; + +// Don't match Wine/Proton infrastructure +// const system_processes = [_][]const u8{ +// "steam.exe", +// "services.exe", +// "winedevice.exe", +// "plugplay.exe", +// "svchost.exe", +// "explorer.exe", +// "rpcss.exe", +// "tabtip.exe", +// "wineboot.exe", +// "rundll32.exe", +// "iexplore.exe", +// "conhost.exe", +// "crashpad_handler.exe", +// "iscriptevaluator.exe", +// "VC_redist.x86.exe", +// "VC_redist.x64.exe", +// "cmd.exe", +// "REDEngineErrorReporter.exe", +// "REDprelauncher.exe", +// "SteamService.exe", +// "UnityCrashHandler64.exe", +// "start.exe", +// }; + +pub fn isProtonParent(arena: std.mem.Allocator, pid: []const u8) !bool { + var path_buf: [std.fs.max_path_bytes]u8 = undefined; + const status_path = try std.fmt.bufPrint(&path_buf, "/proc/{s}/status", .{pid}); + + const file = std.fs.openFileAbsolute(status_path, .{}) catch |err| { + std.log.debug("Failed to open {s}: {}", .{ status_path, err }); + return switch (err) { + error.AccessDenied, error.FileNotFound => false, + else => err, + }; + }; + defer file.close(); + + const content = try file.readToEndAlloc(arena, std.math.maxInt(usize)); + + const ppid_line = std.mem.indexOf(u8, content, "PPid:") orelse return false; + const line_end = std.mem.indexOfScalarPos(u8, content, ppid_line, '\n') orelse content.len; + const ppid_start = ppid_line + 5; // Length of "PPid:" + const ppid = std.mem.trim(u8, content[ppid_start..line_end], " \t"); + + const parent_cmdline_path = try std.fmt.bufPrint(&path_buf, "/proc/{s}/cmdline", .{ppid}); + const parent_file = std.fs.openFileAbsolute(parent_cmdline_path, .{}) catch |err| { + std.log.debug("Failed to open parent cmdline {s}: {}", .{ parent_cmdline_path, err }); + return switch (err) { + error.AccessDenied, error.FileNotFound => false, + else => err, + }; + }; + defer parent_file.close(); + + const parent_content = try parent_file.readToEndAlloc(arena, std.math.maxInt(usize)); + return std.mem.indexOf(u8, parent_content, "proton") != null; +} + +pub fn isProtonGame(process_name: []const u8, config: Config) bool { + if (!std.mem.endsWith(u8, process_name, ".exe")) return false; + + for (config.system_processes) |sys_proc| { + if (std.mem.eql(u8, process_name, sys_proc)) { + return false; + } + } + + return true; +} + +pub fn matchProcess(profiles: []Profile, proton_profile: ?*const Profile, arena: std.mem.Allocator, pid: []const u8, process_name: []const u8, config: Config) !?*const Profile { + const is_exe = std.mem.endsWith(u8, process_name, ".exe"); + var match: ?*const Profile = null; + + for (profiles) |*profile| { + const is_match = profile != proton_profile and profile.matches(process_name); + if (is_match) { + std.log.info("Matched profile {s} for process {s}", .{ profile.name, process_name }); + match = profile; + break; + } + } + + const should_check_proton = match == null and + is_exe and + proton_profile != null; + + if (should_check_proton) { + const is_system = for (config.system_processes) |sys_proc| { + if (std.mem.eql(u8, process_name, sys_proc)) break true; + } else false; + + if (!is_system) { + const is_proton = try isProtonParent(arena, pid); + if (is_proton) { + std.log.info("Found Proton game: {s}", .{process_name}); + match = proton_profile; + } + } + } + + return match; +} diff --git a/falcond/src/profile/types.zig b/falcond/src/profile/types.zig new file mode 100644 index 0000000..6cb933a --- /dev/null +++ b/falcond/src/profile/types.zig @@ -0,0 +1,25 @@ +const std = @import("std"); +const vcache_setting = @import("../clients/vcache_setting.zig"); +const scx_scheds = @import("../clients/scx_scheds.zig"); + +pub const Profile = struct { + name: []const u8, + performance_mode: bool = false, + scx_sched: scx_scheds.ScxScheduler = .none, + scx_sched_props: ?scx_scheds.ScxSchedModes = null, + vcache_mode: vcache_setting.VCacheMode = .cache, + + pub fn matches(self: *const Profile, process_name: []const u8) bool { + const is_match = std.ascii.eqlIgnoreCase(self.name, process_name); + if (is_match) { + std.log.info("Found match: {s} for process {s}", .{ self.name, process_name }); + } + return is_match; + } +}; + +pub const CacheEntry = struct { + pid: u32, + timestamp: i64, + is_proton: bool, +}; diff --git a/main.sh b/main.sh index 37e034a..a9c4a9d 100755 --- a/main.sh +++ b/main.sh @@ -2,7 +2,7 @@ set -e -VERSION="1.0.0" +VERSION="1.0.1" source ./pika-build-config.sh