First release
Some checks failed
PikaOS Package Build & Release (Canary) (amd64-v3) / build (push) Has been cancelled
PikaOS Package Build Only (amd64-v3) / build (push) Has been cancelled
PikaOS Package Build Only (Canary) (amd64-v3) / build (push) Has been cancelled
PikaOS Package Build & Release (amd64-v3) / build (push) Failing after 15s
Some checks failed
PikaOS Package Build & Release (Canary) (amd64-v3) / build (push) Has been cancelled
PikaOS Package Build Only (amd64-v3) / build (push) Has been cancelled
PikaOS Package Build Only (Canary) (amd64-v3) / build (push) Has been cancelled
PikaOS Package Build & Release (amd64-v3) / build (push) Failing after 15s
This commit is contained in:
commit
163e2956bd
1
.github/build-canary-v3
vendored
Normal file
1
.github/build-canary-v3
vendored
Normal file
@ -0,0 +1 @@
|
||||
1
|
1
.github/build-nest-v3
vendored
Normal file
1
.github/build-nest-v3
vendored
Normal file
@ -0,0 +1 @@
|
||||
1
|
1
.github/release-canary-v3
vendored
Normal file
1
.github/release-canary-v3
vendored
Normal file
@ -0,0 +1 @@
|
||||
1
|
1
.github/release-nest-v3
vendored
Normal file
1
.github/release-nest-v3
vendored
Normal file
@ -0,0 +1 @@
|
||||
1
|
40
.github/workflows/build-canaryv3.yml
vendored
Normal file
40
.github/workflows/build-canaryv3.yml
vendored
Normal file
@ -0,0 +1,40 @@
|
||||
name: PikaOS Package Build Only (Canary) (amd64-v3)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- '.github/build-canary-v3'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ghcr.io/pikaos-linux/pikaos-builder:canaryv3
|
||||
volumes:
|
||||
- /proc:/proc
|
||||
options: --privileged -it
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Install SSH key
|
||||
uses: shimataro/ssh-key-action@v2
|
||||
with:
|
||||
key: ${{ secrets.SSH_KEY }}
|
||||
name: id_rsa
|
||||
known_hosts: ${{ secrets.KNOWN_HOSTS }}
|
||||
if_key_exists: replace
|
||||
|
||||
- name: Update APT Cache
|
||||
run: apt-get update -y
|
||||
|
||||
- name: Set Build Config
|
||||
run: cp -vf ./pika-build-config/amd64-v3.sh ./pika-build-config.sh
|
||||
|
||||
- name: Setup Makefile
|
||||
run: cp -vf ./Makefile-v3 ./Makefile
|
||||
|
||||
- name: Build Package
|
||||
run: ./main.sh
|
40
.github/workflows/build-nestv3.yml
vendored
Normal file
40
.github/workflows/build-nestv3.yml
vendored
Normal file
@ -0,0 +1,40 @@
|
||||
name: PikaOS Package Build Only (amd64-v3)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- '.github/build-nest-v3'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ghcr.io/pikaos-linux/pikaos-builder:nestv3
|
||||
volumes:
|
||||
- /proc:/proc
|
||||
options: --privileged -it
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Install SSH key
|
||||
uses: shimataro/ssh-key-action@v2
|
||||
with:
|
||||
key: ${{ secrets.SSH_KEY }}
|
||||
name: id_rsa
|
||||
known_hosts: ${{ secrets.KNOWN_HOSTS }}
|
||||
if_key_exists: replace
|
||||
|
||||
- name: Update APT Cache
|
||||
run: apt-get update -y
|
||||
|
||||
- name: Set Build Config
|
||||
run: cp -vf ./pika-build-config/amd64-v3.sh ./pika-build-config.sh
|
||||
|
||||
- name: Setup Makefile
|
||||
run: cp -vf ./Makefile-v3 ./Makefile
|
||||
|
||||
- name: Build Package
|
||||
run: ./main.sh
|
43
.github/workflows/release-canaryv3.yml
vendored
Normal file
43
.github/workflows/release-canaryv3.yml
vendored
Normal file
@ -0,0 +1,43 @@
|
||||
name: PikaOS Package Build & Release (Canary) (amd64-v3)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- '.github/release-canary-v3'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ghcr.io/pikaos-linux/pikaos-builder:canaryv3
|
||||
volumes:
|
||||
- /proc:/proc
|
||||
options: --privileged -it
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Install SSH key
|
||||
uses: shimataro/ssh-key-action@v2
|
||||
with:
|
||||
key: ${{ secrets.SSH_KEY }}
|
||||
name: id_rsa
|
||||
known_hosts: ${{ secrets.KNOWN_HOSTS }}
|
||||
if_key_exists: replace
|
||||
|
||||
- name: Update APT Cache
|
||||
run: apt-get update -y
|
||||
|
||||
- name: Set Build Config
|
||||
run: cp -vf ./pika-build-config/amd64-v3.sh ./pika-build-config.sh
|
||||
|
||||
- name: Setup Makefile
|
||||
run: cp -vf ./Makefile-v3 ./Makefile
|
||||
|
||||
- name: Build Package
|
||||
run: ./main.sh
|
||||
|
||||
- name: Release Package
|
||||
run: ./release.sh
|
40
.github/workflows/release-nestv3.yml
vendored
Normal file
40
.github/workflows/release-nestv3.yml
vendored
Normal file
@ -0,0 +1,40 @@
|
||||
name: PikaOS Package Build & Release (amd64-v3)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- '.github/release-nest-v3'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ghcr.io/pikaos-linux/pikaos-builder:nestv3
|
||||
volumes:
|
||||
- /proc:/proc
|
||||
options: --privileged -it
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Install SSH key
|
||||
uses: shimataro/ssh-key-action@v2
|
||||
with:
|
||||
key: ${{ secrets.SSH_KEY }}
|
||||
name: id_rsa
|
||||
known_hosts: ${{ secrets.KNOWN_HOSTS }}
|
||||
if_key_exists: replace
|
||||
|
||||
- name: Update APT Cache
|
||||
run: apt-get update -y
|
||||
|
||||
- name: Set Build Config
|
||||
run: cp -vf ./pika-build-config/amd64-v3.sh ./pika-build-config.sh
|
||||
|
||||
- name: Build Package
|
||||
run: ./main.sh
|
||||
|
||||
- name: Release Package
|
||||
run: ./release.sh
|
18
.gitignore
vendored
Normal file
18
.gitignore
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
.idea
|
||||
zig-out/
|
||||
.zig-cache/
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 PikaOS
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
17
falcond/build.zig
Normal file
17
falcond/build.zig
Normal file
@ -0,0 +1,17 @@
|
||||
const std = @import("std");
|
||||
|
||||
pub fn build(b: *std.Build) void {
|
||||
const target = b.standardTargetOptions(.{});
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
|
||||
const exe = b.addExecutable(.{
|
||||
.name = "falcond",
|
||||
.root_source_file = .{ .cwd_relative = "src/main.zig" },
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
|
||||
exe.bundle_compiler_rt = true;
|
||||
|
||||
b.installArtifact(exe);
|
||||
}
|
72
falcond/build.zig.zon
Normal file
72
falcond/build.zig.zon
Normal file
@ -0,0 +1,72 @@
|
||||
.{
|
||||
// This is the default name used by packages depending on this one. For
|
||||
// example, when a user runs `zig fetch --save <url>`, this field is used
|
||||
// as the key in the `dependencies` table. Although the user can choose a
|
||||
// different name, most users will stick with this provided value.
|
||||
//
|
||||
// It is redundant to include "zig" in this name because it is already
|
||||
// within the Zig package namespace.
|
||||
.name = "falcond",
|
||||
|
||||
// This is a [Semantic Version](https://semver.org/).
|
||||
// In a future version of Zig it will be used for package deduplication.
|
||||
.version = "0.1.0",
|
||||
|
||||
// This field is optional.
|
||||
// This is currently advisory only; Zig does not yet do anything
|
||||
// with this value.
|
||||
//.minimum_zig_version = "0.11.0",
|
||||
|
||||
// This field is optional.
|
||||
// Each dependency must either provide a `url` and `hash`, or a `path`.
|
||||
// `zig build --fetch` can be used to fetch all dependencies of a package, recursively.
|
||||
// Once all dependencies are fetched, `zig build` no longer requires
|
||||
// internet connectivity.
|
||||
.dependencies = .{
|
||||
// See `zig fetch --save <url>` for a command-line interface for adding dependencies.
|
||||
//.example = .{
|
||||
// // When updating this field to a new URL, be sure to delete the corresponding
|
||||
// // `hash`, otherwise you are communicating that you expect to find the old hash at
|
||||
// // the new URL.
|
||||
// .url = "https://example.com/foo.tar.gz",
|
||||
//
|
||||
// // This is computed from the file contents of the directory of files that is
|
||||
// // obtained after fetching `url` and applying the inclusion rules given by
|
||||
// // `paths`.
|
||||
// //
|
||||
// // This field is the source of truth; packages do not come from a `url`; they
|
||||
// // come from a `hash`. `url` is just one of many possible mirrors for how to
|
||||
// // obtain a package matching this `hash`.
|
||||
// //
|
||||
// // Uses the [multihash](https://multiformats.io/multihash/) format.
|
||||
// .hash = "...",
|
||||
//
|
||||
// // When this is provided, the package is found in a directory relative to the
|
||||
// // build root. In this case the package's hash is irrelevant and therefore not
|
||||
// // computed. This field and `url` are mutually exclusive.
|
||||
// .path = "foo",
|
||||
|
||||
// // When this is set to `true`, a package is declared to be lazily
|
||||
// // fetched. This makes the dependency only get fetched if it is
|
||||
// // actually used.
|
||||
// .lazy = false,
|
||||
//},
|
||||
},
|
||||
|
||||
// Specifies the set of files and directories that are included in this package.
|
||||
// Only files and directories listed here are included in the `hash` that
|
||||
// is computed for this package. Only files listed here will remain on disk
|
||||
// when using the zig package manager. As a rule of thumb, one should list
|
||||
// files required for compilation plus any license(s).
|
||||
// Paths are relative to the build root. Use the empty string (`""`) to refer to
|
||||
// the build root itself.
|
||||
// A directory listed here means that all files within, recursively, are included.
|
||||
.paths = .{
|
||||
"build.zig",
|
||||
"build.zig.zon",
|
||||
"src",
|
||||
// For example...
|
||||
//"LICENSE",
|
||||
//"README.md",
|
||||
},
|
||||
}
|
5
falcond/debian/changelog
Normal file
5
falcond/debian/changelog
Normal file
@ -0,0 +1,5 @@
|
||||
falcon (1.0.0-101pika1) pika; urgency=low
|
||||
|
||||
* Initial release
|
||||
|
||||
-- ferreo <ferreo@pika-os.com> Sun, 12 Jan 2025 13:48:00 +0300
|
20
falcond/debian/control
Normal file
20
falcond/debian/control
Normal file
@ -0,0 +1,20 @@
|
||||
Source: falcond
|
||||
Section: admin
|
||||
Priority: optional
|
||||
Maintainer: ferreo <ferreo@pika-os.com>
|
||||
Rules-Requires-Root: no
|
||||
Build-Depends:
|
||||
debhelper-compat (= 13), zig-nightly, libdbus-1-dev, dh-systemd, git
|
||||
Standards-Version: 4.6.1
|
||||
Homepage: https://pika-os.com
|
||||
|
||||
Package: falcond
|
||||
Architecture: amd64
|
||||
Depends: ${misc:Depends},
|
||||
${shlibs:Depends},
|
||||
util-linux,
|
||||
power-profiles-daemon | tuned-ppd,
|
||||
scx,
|
||||
systemd
|
||||
Provides: falcond
|
||||
Description: Accelerate your gaming experience with falcond, auto setting scx, vcache and choosing performance profiles
|
21
falcond/debian/copyright
Normal file
21
falcond/debian/copyright
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 PikaOS
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
13
falcond/debian/falcond.service
Normal file
13
falcond/debian/falcond.service
Normal file
@ -0,0 +1,13 @@
|
||||
[Unit]
|
||||
Description=Falcon Daemon Service
|
||||
After=network.target
|
||||
Wants=graphical.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/bin/falcond
|
||||
User=root
|
||||
Restart=on-failure
|
||||
|
||||
[Install]
|
||||
WantedBy=graphical.target
|
34
falcond/debian/rules
Executable file
34
falcond/debian/rules
Executable file
@ -0,0 +1,34 @@
|
||||
#!/usr/bin/make -f
|
||||
|
||||
# See debhelper(7) (uncomment to enable).
|
||||
# Output every command that modifies files on the build system.
|
||||
export DH_VERBOSE = 1
|
||||
|
||||
%:
|
||||
dh $@ --with=systemd
|
||||
|
||||
override_dh_dwz:
|
||||
echo "disabled"
|
||||
|
||||
override_dh_auto_build:
|
||||
zig build-exe src/main.zig -O ReleaseFast -mcpu x86_64_v3 --name falcond
|
||||
# Clone profiles repository
|
||||
rm -rf falcond-profiles
|
||||
git clone https://github.com/PikaOS-Linux/falcond-profiles.git
|
||||
|
||||
override_dh_install:
|
||||
dh_install
|
||||
mkdir -p debian/falcond/usr/bin/
|
||||
mkdir -p debian/falcond/usr/share/falcond/
|
||||
cp -vf falcond debian/falcond/usr/bin/
|
||||
chmod 755 debian/falcond/usr/bin/falcond
|
||||
chmod +x debian/falcond/usr/bin/falcond
|
||||
# Copy profiles
|
||||
cp -r falcond-profiles/usr/share/falcond/profiles debian/falcond/usr/share/falcond/
|
||||
|
||||
override_dh_installsystemd:
|
||||
dh_installsystemd --name=falcond
|
||||
|
||||
override_dh_clean:
|
||||
dh_clean
|
||||
rm -rf falcond-profiles
|
1
falcond/debian/source/format
Normal file
1
falcond/debian/source/format
Normal file
@ -0,0 +1 @@
|
||||
3.0 (quilt)
|
52
falcond/src/config.zig
Normal file
52
falcond/src/config.zig
Normal file
@ -0,0 +1,52 @@
|
||||
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 = .gaming,
|
||||
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("scx_sched_props = {s}\n", .{@tagName(self.scx_sched_props)});
|
||||
try file.writer().print("vcache_mode = {s}\n", .{@tagName(self.vcache_mode)});
|
||||
}
|
||||
};
|
54
falcond/src/confloader.zig
Normal file
54
falcond/src/confloader.zig
Normal file
@ -0,0 +1,54 @@
|
||||
const std = @import("std");
|
||||
const Parser = @import("parser.zig").Parser;
|
||||
|
||||
pub fn loadConf(comptime T: type, allocator: std.mem.Allocator, path: []const u8) !T {
|
||||
const file = try std.fs.openFileAbsolute(path, .{
|
||||
.mode = .read_only,
|
||||
.lock = .none,
|
||||
.lock_nonblocking = false,
|
||||
});
|
||||
defer file.close();
|
||||
|
||||
const size = try file.getEndPos();
|
||||
if (size > std.math.maxInt(u32)) return error.FileTooLarge;
|
||||
|
||||
var stack_buffer: [4096]u8 = undefined;
|
||||
const buffer = if (size <= stack_buffer.len) stack_buffer[0..size] else try allocator.alloc(u8, size);
|
||||
defer if (size > stack_buffer.len) allocator.free(buffer);
|
||||
|
||||
const bytes_read = try file.readAll(buffer);
|
||||
if (bytes_read != size) return error.UnexpectedEOF;
|
||||
|
||||
var parser = Parser(T).init(allocator, buffer);
|
||||
return try parser.parse();
|
||||
}
|
||||
|
||||
pub fn loadConfDir(comptime T: type, allocator: std.mem.Allocator, dir_path: []const u8) !std.ArrayList(T) {
|
||||
var result = std.ArrayList(T).init(allocator);
|
||||
errdefer result.deinit();
|
||||
|
||||
var dir = try std.fs.openDirAbsolute(dir_path, .{ .iterate = true });
|
||||
defer dir.close();
|
||||
|
||||
var walker = try dir.walk(allocator);
|
||||
defer walker.deinit();
|
||||
|
||||
while (try walker.next()) |entry| {
|
||||
if (entry.kind == .file and std.mem.endsWith(u8, entry.path, ".conf")) {
|
||||
const path = try std.fs.path.join(allocator, &.{ dir_path, entry.path });
|
||||
defer allocator.free(path);
|
||||
|
||||
const file = try std.fs.openFileAbsolute(path, .{});
|
||||
defer file.close();
|
||||
|
||||
const content = try file.readToEndAlloc(allocator, std.math.maxInt(usize));
|
||||
defer allocator.free(content);
|
||||
|
||||
var parser = Parser(T).init(allocator, content);
|
||||
const parsed = try parser.parse();
|
||||
try result.append(parsed);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
211
falcond/src/daemon.zig
Normal file
211
falcond/src/daemon.zig
Normal file
@ -0,0 +1,211 @@
|
||||
const std = @import("std");
|
||||
const ProfileManager = @import("profile.zig").ProfileManager;
|
||||
const Profile = @import("profile.zig").Profile;
|
||||
const Config = @import("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");
|
||||
|
||||
pub const Daemon = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
profile_manager: ProfileManager,
|
||||
oneshot: bool,
|
||||
known_pids: ?std.AutoHashMap(u32, *const Profile),
|
||||
power_profiles: *PowerProfiles,
|
||||
|
||||
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);
|
||||
|
||||
try scx_scheds.init(allocator);
|
||||
|
||||
return Self{
|
||||
.allocator = allocator,
|
||||
.profile_manager = profile_manager,
|
||||
.oneshot = oneshot,
|
||||
.known_pids = null,
|
||||
.power_profiles = power_profiles,
|
||||
};
|
||||
}
|
||||
|
||||
fn scanProcesses(allocator: std.mem.Allocator) !std.AutoHashMap(u32, []const u8) {
|
||||
var pids = std.AutoHashMap(u32, []const u8).init(allocator);
|
||||
|
||||
const proc_fd = try std.posix.open("/proc", .{
|
||||
.ACCMODE = .RDONLY,
|
||||
.DIRECTORY = true,
|
||||
}, 0);
|
||||
defer std.posix.close(proc_fd);
|
||||
|
||||
var buffer: [8192]u8 = undefined;
|
||||
while (true) {
|
||||
const nread = linux.syscall3(.getdents64, @as(usize, @intCast(proc_fd)), @intFromPtr(&buffer), buffer.len);
|
||||
|
||||
if (nread == 0) break;
|
||||
if (nread < 0) return error.ReadDirError;
|
||||
|
||||
var pos: usize = 0;
|
||||
while (pos < nread) {
|
||||
const dirent = @as(*align(1) linux.dirent64, @ptrCast(&buffer[pos]));
|
||||
if (dirent.type == linux.DT.DIR) {
|
||||
const name = std.mem.sliceTo(@as([*:0]u8, @ptrCast(&dirent.name)), 0);
|
||||
if (std.fmt.parseInt(u32, name, 10)) |pid| {
|
||||
if (getProcessNameFromPid(allocator, pid)) |proc_name| {
|
||||
try pids.put(pid, proc_name);
|
||||
} else |_| {}
|
||||
} else |_| {}
|
||||
}
|
||||
pos += dirent.reclen;
|
||||
}
|
||||
}
|
||||
|
||||
return pids;
|
||||
}
|
||||
|
||||
fn getProcessNameFromPid(allocator: std.mem.Allocator, pid: u32) ![]const u8 {
|
||||
var path_buf: [64]u8 = undefined;
|
||||
const path = try std.fmt.bufPrint(&path_buf, "/proc/{d}/cmdline", .{pid});
|
||||
|
||||
const file = try std.fs.openFileAbsolute(path, .{});
|
||||
defer file.close();
|
||||
|
||||
var buffer: [4096]u8 = undefined;
|
||||
const bytes = try file.readAll(&buffer);
|
||||
if (bytes == 0) return error.EmptyFile;
|
||||
|
||||
const end = std.mem.indexOfScalar(u8, buffer[0..bytes], 0) orelse bytes;
|
||||
const cmdline = buffer[0..end];
|
||||
|
||||
const last_unix = std.mem.lastIndexOfScalar(u8, cmdline, '/') orelse 0;
|
||||
const last_windows = std.mem.lastIndexOfScalar(u8, cmdline, '\\') orelse 0;
|
||||
const last_sep = @max(last_unix, last_windows);
|
||||
|
||||
const exe_name = if (last_sep > 0)
|
||||
cmdline[last_sep + 1 ..]
|
||||
else
|
||||
cmdline;
|
||||
|
||||
return try allocator.dupe(u8, exe_name);
|
||||
}
|
||||
|
||||
pub fn checkProcesses(self: *Self) !void {
|
||||
var arena = std.heap.ArenaAllocator.init(self.allocator);
|
||||
defer arena.deinit();
|
||||
const arena_allocator = arena.allocator();
|
||||
|
||||
var processes = try scanProcesses(arena_allocator);
|
||||
defer {
|
||||
var it = processes.iterator();
|
||||
while (it.next()) |entry| {
|
||||
arena_allocator.free(entry.value_ptr.*);
|
||||
}
|
||||
processes.deinit();
|
||||
}
|
||||
|
||||
var it = processes.iterator();
|
||||
while (it.next()) |entry| {
|
||||
const pid = entry.key_ptr.*;
|
||||
const process_name = entry.value_ptr.*;
|
||||
|
||||
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| {
|
||||
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 (!self.oneshot) {
|
||||
if (self.known_pids) |*known| {
|
||||
var known_it = known.iterator();
|
||||
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}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ = pids.remove(pid_num);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
216
falcond/src/dbus.zig
Normal file
216
falcond/src/dbus.zig
Normal file
@ -0,0 +1,216 @@
|
||||
const std = @import("std");
|
||||
|
||||
pub const DBusError = error{
|
||||
CommandFailed,
|
||||
ParseError,
|
||||
InvalidValue,
|
||||
NoConnection,
|
||||
} || std.fs.File.OpenError || std.posix.WriteError || std.posix.ReadError || std.process.Child.RunError || std.process.Child.SpawnError || error{
|
||||
ProcessFdQuotaExceeded,
|
||||
SystemFdQuotaExceeded,
|
||||
SystemResources,
|
||||
OperationAborted,
|
||||
WouldBlock,
|
||||
InvalidHandle,
|
||||
Unexpected,
|
||||
InputOutput,
|
||||
OutOfMemory,
|
||||
ResourceLimitReached,
|
||||
StderrStreamTooLong,
|
||||
StdoutStreamTooLong,
|
||||
CurrentWorkingDirectoryUnlinked,
|
||||
InvalidBatchScriptArg,
|
||||
InvalidExe,
|
||||
FileSystem,
|
||||
Overflow,
|
||||
InvalidCharacter,
|
||||
InvalidUserId,
|
||||
PermissionDenied,
|
||||
ProcessAlreadyExec,
|
||||
InvalidProcessGroupId,
|
||||
InvalidName,
|
||||
WaitAbandoned,
|
||||
WaitTimeOut,
|
||||
NetworkSubsystemFailed,
|
||||
};
|
||||
|
||||
/// Simple DBus interface that uses busctl under the hood
|
||||
pub const DBus = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
bus_name: []const u8,
|
||||
object_path: []const u8,
|
||||
interface: []const u8,
|
||||
|
||||
pub fn init(
|
||||
allocator: std.mem.Allocator,
|
||||
bus_name: []const u8,
|
||||
object_path: []const u8,
|
||||
interface: []const u8,
|
||||
) DBus {
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
.bus_name = bus_name,
|
||||
.object_path = object_path,
|
||||
.interface = interface,
|
||||
};
|
||||
}
|
||||
|
||||
/// Get a property value as a string
|
||||
pub fn getProperty(self: *const DBus, property: []const u8) ![]const u8 {
|
||||
var argv = [_][]const u8{
|
||||
"busctl",
|
||||
"--system",
|
||||
"get-property",
|
||||
self.bus_name,
|
||||
self.object_path,
|
||||
self.interface,
|
||||
property,
|
||||
};
|
||||
|
||||
const max_output_size = 1024 * 1024; // 1MB should be enough
|
||||
const output = try std.process.Child.run(.{
|
||||
.allocator = self.allocator,
|
||||
.argv = &argv,
|
||||
.max_output_bytes = max_output_size,
|
||||
});
|
||||
defer self.allocator.free(output.stderr);
|
||||
defer self.allocator.free(output.stdout);
|
||||
|
||||
if (output.term.Exited != 0) {
|
||||
std.log.err("busctl failed: {s}", .{output.stderr});
|
||||
return DBusError.CommandFailed;
|
||||
}
|
||||
|
||||
// busctl outputs values in two formats:
|
||||
// 1. String: "s \"value\""
|
||||
// 2. Integer: "u 123"
|
||||
const trimmed = std.mem.trim(u8, output.stdout, " \n\r\t");
|
||||
|
||||
// Try string format first
|
||||
var it = std.mem.splitScalar(u8, trimmed, '"');
|
||||
_ = it.next(); // Skip type
|
||||
if (it.next()) |value| {
|
||||
return self.allocator.dupe(u8, value);
|
||||
}
|
||||
|
||||
// Try integer format
|
||||
it = std.mem.splitScalar(u8, trimmed, ' ');
|
||||
_ = it.next(); // Skip type
|
||||
const value = it.next() orelse {
|
||||
// If property doesn't exist or is empty
|
||||
if (std.mem.indexOf(u8, output.stdout, "Unknown property") != null) {
|
||||
return self.allocator.dupe(u8, "");
|
||||
}
|
||||
return DBusError.ParseError;
|
||||
};
|
||||
|
||||
return self.allocator.dupe(u8, value);
|
||||
}
|
||||
|
||||
/// Get a property value as a string array
|
||||
pub fn getPropertyArray(self: *const DBus, property: []const u8) ![][]const u8 {
|
||||
var result = std.ArrayList([]const u8).init(self.allocator);
|
||||
errdefer {
|
||||
for (result.items) |item| {
|
||||
self.allocator.free(item);
|
||||
}
|
||||
result.deinit();
|
||||
}
|
||||
|
||||
const argv = [_][]const u8{
|
||||
"busctl",
|
||||
"--system",
|
||||
"get-property",
|
||||
self.bus_name,
|
||||
self.object_path,
|
||||
self.interface,
|
||||
property,
|
||||
};
|
||||
|
||||
const max_output_size = 1024 * 1024; // 1MB should be enough
|
||||
const output = try std.process.Child.run(.{
|
||||
.allocator = self.allocator,
|
||||
.argv = &argv,
|
||||
.max_output_bytes = max_output_size,
|
||||
});
|
||||
defer self.allocator.free(output.stderr);
|
||||
defer self.allocator.free(output.stdout);
|
||||
|
||||
if (output.term.Exited != 0) {
|
||||
std.log.err("busctl failed: {s}", .{output.stderr});
|
||||
return DBusError.CommandFailed;
|
||||
}
|
||||
|
||||
// busctl outputs arrays in the format:
|
||||
// as 2 "value1" "value2"
|
||||
var it = std.mem.splitScalar(u8, std.mem.trim(u8, output.stdout, " \n\r\t"), '"');
|
||||
_ = it.next(); // Skip type + count
|
||||
|
||||
while (it.next()) |value| {
|
||||
// Skip empty strings and spaces between quotes
|
||||
if (value.len == 0 or std.mem.eql(u8, std.mem.trim(u8, value, " "), "")) continue;
|
||||
try result.append(try self.allocator.dupe(u8, value));
|
||||
}
|
||||
|
||||
return result.toOwnedSlice();
|
||||
}
|
||||
|
||||
/// Set a property value
|
||||
pub fn setProperty(self: *const DBus, property: []const u8, value: []const u8) !void {
|
||||
const argv = [_][]const u8{
|
||||
"busctl",
|
||||
"--system",
|
||||
"set-property",
|
||||
self.bus_name,
|
||||
self.object_path,
|
||||
self.interface,
|
||||
property,
|
||||
"s",
|
||||
value,
|
||||
};
|
||||
|
||||
const max_output_size = 1024; // Small size since we don't expect much output
|
||||
const output = try std.process.Child.run(.{
|
||||
.allocator = self.allocator,
|
||||
.argv = &argv,
|
||||
.max_output_bytes = max_output_size,
|
||||
});
|
||||
defer self.allocator.free(output.stderr);
|
||||
defer self.allocator.free(output.stdout);
|
||||
|
||||
if (output.term.Exited != 0) {
|
||||
std.log.err("busctl failed: {s}", .{output.stderr});
|
||||
return DBusError.CommandFailed;
|
||||
}
|
||||
}
|
||||
|
||||
/// Call a DBus method
|
||||
pub fn callMethod(self: *const DBus, method: []const u8, args: []const []const u8) !void {
|
||||
var argv = std.ArrayList([]const u8).init(self.allocator);
|
||||
defer argv.deinit();
|
||||
|
||||
try argv.appendSlice(&[_][]const u8{
|
||||
"busctl",
|
||||
"--system",
|
||||
"call",
|
||||
self.bus_name,
|
||||
self.object_path,
|
||||
self.interface,
|
||||
method,
|
||||
});
|
||||
|
||||
try argv.appendSlice(args);
|
||||
|
||||
const result = try std.process.Child.run(.{
|
||||
.allocator = self.allocator,
|
||||
.argv = argv.items,
|
||||
});
|
||||
defer self.allocator.free(result.stderr);
|
||||
defer self.allocator.free(result.stdout);
|
||||
|
||||
if (result.term.Exited != 0) {
|
||||
std.log.err("busctl failed: {s}", .{result.stderr});
|
||||
return DBusError.CommandFailed;
|
||||
}
|
||||
}
|
||||
};
|
104
falcond/src/main.zig
Normal file
104
falcond/src/main.zig
Normal file
@ -0,0 +1,104 @@
|
||||
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,
|
||||
.log_scope_levels = &[_]std.log.ScopeLevel{
|
||||
.{ .scope = .default, .level = .debug },
|
||||
},
|
||||
};
|
||||
|
||||
const AllocTracker = struct {
|
||||
allocs: usize = 0,
|
||||
deallocs: usize = 0,
|
||||
resizes: usize = 0,
|
||||
|
||||
pub fn trackAlloc(self: *@This()) void {
|
||||
self.allocs += 1;
|
||||
}
|
||||
|
||||
pub fn trackDealloc(self: *@This()) void {
|
||||
self.deallocs += 1;
|
||||
}
|
||||
|
||||
pub fn trackResize(self: *@This()) void {
|
||||
self.resizes += 1;
|
||||
}
|
||||
};
|
||||
|
||||
fn alloc(ctx: *anyopaque, len: usize, ptr_align: u8, ret_addr: usize) ?[*]u8 {
|
||||
var t: *AllocTracker = @ptrCast(@alignCast(ctx));
|
||||
t.trackAlloc();
|
||||
return gpa_vtable.alloc(gpa_ptr, len, ptr_align, ret_addr);
|
||||
}
|
||||
|
||||
fn resize(ctx: *anyopaque, buf: []u8, log2_buf_align: u8, new_len: usize, ret_addr: usize) bool {
|
||||
var t: *AllocTracker = @ptrCast(@alignCast(ctx));
|
||||
t.trackResize();
|
||||
return gpa_vtable.resize(gpa_ptr, buf, log2_buf_align, new_len, ret_addr);
|
||||
}
|
||||
|
||||
fn free(ctx: *anyopaque, buf: []u8, log2_buf_align: u8, ret_addr: usize) void {
|
||||
var t: *AllocTracker = @ptrCast(@alignCast(ctx));
|
||||
t.trackDealloc();
|
||||
gpa_vtable.free(gpa_ptr, buf, log2_buf_align, ret_addr);
|
||||
}
|
||||
|
||||
var gpa_vtable: *const std.mem.Allocator.VTable = undefined;
|
||||
var gpa_ptr: *anyopaque = undefined;
|
||||
|
||||
pub fn main() !void {
|
||||
std.log.info("Starting falcond...", .{});
|
||||
|
||||
var tracker = AllocTracker{};
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{
|
||||
.verbose_log = false,
|
||||
.enable_memory_limit = true,
|
||||
}){};
|
||||
defer {
|
||||
const leaked = gpa.deinit();
|
||||
if (leaked == .leak) {
|
||||
std.log.err("Memory leaks detected!", .{});
|
||||
}
|
||||
std.log.info("Memory operations - allocs: {}, deallocs: {}, resizes: {}", .{
|
||||
tracker.allocs,
|
||||
tracker.deallocs,
|
||||
tracker.resizes,
|
||||
});
|
||||
}
|
||||
|
||||
gpa_vtable = gpa.allocator().vtable;
|
||||
gpa_ptr = gpa.allocator().ptr;
|
||||
const allocator = std.mem.Allocator{
|
||||
.ptr = &tracker,
|
||||
.vtable = &std.mem.Allocator.VTable{
|
||||
.alloc = alloc,
|
||||
.resize = resize,
|
||||
.free = free,
|
||||
},
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
const oneshot = for (args) |arg| {
|
||||
if (std.mem.eql(u8, arg, "--oneshot")) break true;
|
||||
} else false;
|
||||
|
||||
var daemon = try Daemon.init(allocator, &config, oneshot, power_profiles);
|
||||
defer daemon.deinit();
|
||||
|
||||
try daemon.run();
|
||||
}
|
366
falcond/src/parser.zig
Normal file
366
falcond/src/parser.zig
Normal file
@ -0,0 +1,366 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const Vector = std.meta.Vector;
|
||||
|
||||
pub const ParseError = error{
|
||||
InvalidSyntax,
|
||||
UnexpectedCharacter,
|
||||
UnterminatedString,
|
||||
InvalidNumber,
|
||||
InvalidIdentifier,
|
||||
UnknownField,
|
||||
};
|
||||
|
||||
pub fn Parser(comptime T: type) type {
|
||||
return struct {
|
||||
const Self = @This();
|
||||
|
||||
content: []const u8,
|
||||
pos: usize = 0,
|
||||
allocator: std.mem.Allocator,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, content: []const u8) Self {
|
||||
return .{
|
||||
.content = content,
|
||||
.allocator = allocator,
|
||||
};
|
||||
}
|
||||
|
||||
fn skipWhitespace(self: *Self) void {
|
||||
const v_size = std.simd.suggestVectorLength(u8) orelse 32;
|
||||
const Vec = @Vector(v_size, u8);
|
||||
|
||||
while (self.pos + v_size <= self.content.len) {
|
||||
const chunk: Vec = self.content[self.pos..][0..v_size].*;
|
||||
const spaces = chunk == @as(Vec, @splat(@as(u8, ' ')));
|
||||
const tabs = chunk == @as(Vec, @splat(@as(u8, '\t')));
|
||||
const newlines = chunk == @as(Vec, @splat(@as(u8, '\n')));
|
||||
const returns = chunk == @as(Vec, @splat(@as(u8, '\r')));
|
||||
const comments = chunk == @as(Vec, @splat(@as(u8, '#')));
|
||||
|
||||
const whitespace = @reduce(.Or, spaces) or @reduce(.Or, tabs) or
|
||||
@reduce(.Or, newlines) or @reduce(.Or, returns) or
|
||||
@reduce(.Or, comments);
|
||||
if (!whitespace) break;
|
||||
|
||||
if (@reduce(.Or, comments)) {
|
||||
while (self.pos < self.content.len and self.content[self.pos] != '\n') : (self.pos += 1) {}
|
||||
continue;
|
||||
}
|
||||
|
||||
const space_mask = @select(u8, spaces, @as(Vec, @splat(@as(u8, 1))), @as(Vec, @splat(@as(u8, 0))));
|
||||
const tab_mask = @select(u8, tabs, @as(Vec, @splat(@as(u8, 1))), @as(Vec, @splat(@as(u8, 0))));
|
||||
const newline_mask = @select(u8, newlines, @as(Vec, @splat(@as(u8, 1))), @as(Vec, @splat(@as(u8, 0))));
|
||||
const return_mask = @select(u8, returns, @as(Vec, @splat(@as(u8, 1))), @as(Vec, @splat(@as(u8, 0))));
|
||||
|
||||
const mask = space_mask | tab_mask | newline_mask | return_mask;
|
||||
const mask_bits = @reduce(.Or, mask);
|
||||
const leading = @ctz(mask_bits);
|
||||
if (leading == v_size) {
|
||||
self.pos += v_size;
|
||||
} else {
|
||||
self.pos += leading;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
while (self.pos < self.content.len) : (self.pos += 1) {
|
||||
const c = self.content[self.pos];
|
||||
switch (c) {
|
||||
' ', '\t', '\r', '\n' => continue,
|
||||
'#' => {
|
||||
while (self.pos < self.content.len and self.content[self.pos] != '\n') : (self.pos += 1) {}
|
||||
},
|
||||
else => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parseString(self: *Self) ![]const u8 {
|
||||
if (self.pos >= self.content.len or self.content[self.pos] != '"')
|
||||
return error.InvalidSyntax;
|
||||
|
||||
self.pos += 1;
|
||||
const start = self.pos;
|
||||
|
||||
while (self.pos < self.content.len) : (self.pos += 1) {
|
||||
switch (self.content[self.pos]) {
|
||||
'"' => {
|
||||
const str = try self.allocator.dupe(u8, self.content[start..self.pos]);
|
||||
self.pos += 1;
|
||||
return str;
|
||||
},
|
||||
'\\' => return error.InvalidSyntax,
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
return error.UnterminatedString;
|
||||
}
|
||||
|
||||
fn parseNumber(self: *Self) !i64 {
|
||||
self.skipWhitespace();
|
||||
const start = self.pos;
|
||||
while (self.pos < self.content.len) : (self.pos += 1) {
|
||||
const c = self.content[self.pos];
|
||||
if (!std.ascii.isDigit(c) and c != '-') break;
|
||||
}
|
||||
const num = std.fmt.parseInt(i64, self.content[start..self.pos], 10) catch return error.InvalidNumber;
|
||||
return num;
|
||||
}
|
||||
|
||||
fn parseArray(self: *Self) ![]const i64 {
|
||||
if (self.pos >= self.content.len or self.content[self.pos] != '[')
|
||||
return error.InvalidSyntax;
|
||||
|
||||
self.pos += 1;
|
||||
var values: [32]i64 = undefined;
|
||||
var count: usize = 0;
|
||||
|
||||
while (self.pos < self.content.len) {
|
||||
self.skipWhitespace();
|
||||
if (self.content[self.pos] == ']') {
|
||||
self.pos += 1;
|
||||
return try self.allocator.dupe(i64, values[0..count]);
|
||||
}
|
||||
|
||||
if (count >= values.len) return error.InvalidSyntax;
|
||||
const num = try self.parseNumber();
|
||||
values[count] = num;
|
||||
count += 1;
|
||||
|
||||
self.skipWhitespace();
|
||||
if (self.content[self.pos] == ',') {
|
||||
self.pos += 1;
|
||||
continue;
|
||||
}
|
||||
if (self.content[self.pos] == ']') {
|
||||
self.pos += 1;
|
||||
return try self.allocator.dupe(i64, values[0..count]);
|
||||
}
|
||||
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;
|
||||
const Vec = @Vector(v_size, u8);
|
||||
|
||||
while (self.pos + v_size <= self.content.len) {
|
||||
const chunk: Vec = self.content[self.pos..][0..v_size].*;
|
||||
|
||||
const lower_bound = chunk >= @as(Vec, @splat(@as(u8, 'a')));
|
||||
const upper_bound = chunk <= @as(Vec, @splat(@as(u8, 'z')));
|
||||
const alpha_lower_mask = @select(u8, lower_bound, @as(Vec, @splat(@as(u8, 1))), @as(Vec, @splat(@as(u8, 0)))) &
|
||||
@select(u8, upper_bound, @as(Vec, @splat(@as(u8, 1))), @as(Vec, @splat(@as(u8, 0))));
|
||||
|
||||
const upper_lower = chunk >= @as(Vec, @splat(@as(u8, 'A')));
|
||||
const upper_upper = chunk <= @as(Vec, @splat(@as(u8, 'Z')));
|
||||
const alpha_upper_mask = @select(u8, upper_lower, @as(Vec, @splat(@as(u8, 1))), @as(Vec, @splat(@as(u8, 0)))) &
|
||||
@select(u8, upper_upper, @as(Vec, @splat(@as(u8, 1))), @as(Vec, @splat(@as(u8, 0))));
|
||||
|
||||
const digit_lower = chunk >= @as(Vec, @splat(@as(u8, '0')));
|
||||
const digit_upper = chunk <= @as(Vec, @splat(@as(u8, '9')));
|
||||
const digit_mask = @select(u8, digit_lower, @as(Vec, @splat(@as(u8, 1))), @as(Vec, @splat(@as(u8, 0)))) &
|
||||
@select(u8, digit_upper, @as(Vec, @splat(@as(u8, 1))), @as(Vec, @splat(@as(u8, 0))));
|
||||
|
||||
const underscore_mask = @select(u8, chunk == @as(Vec, @splat(@as(u8, '_'))), @as(Vec, @splat(@as(u8, 1))), @as(Vec, @splat(@as(u8, 0))));
|
||||
|
||||
const mask = alpha_lower_mask | alpha_upper_mask | digit_mask | underscore_mask;
|
||||
const valid = @reduce(.Or, mask) != 0;
|
||||
if (!valid) break;
|
||||
|
||||
const mask_bits = @reduce(.Or, mask);
|
||||
const leading = @ctz(mask_bits);
|
||||
if (leading == v_size) {
|
||||
self.pos += v_size;
|
||||
} else {
|
||||
self.pos += leading;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
while (self.pos < self.content.len) : (self.pos += 1) {
|
||||
const c = self.content[self.pos];
|
||||
if (!std.ascii.isAlphabetic(c) and !std.ascii.isDigit(c) and c != '_') break;
|
||||
}
|
||||
|
||||
if (start == self.pos) return error.InvalidIdentifier;
|
||||
return self.content[start..self.pos];
|
||||
}
|
||||
|
||||
pub fn parse(self: *Self) !T {
|
||||
var result: T = std.mem.zeroInit(T, .{});
|
||||
|
||||
while (self.pos < self.content.len) {
|
||||
self.skipWhitespace();
|
||||
if (self.pos >= self.content.len) break;
|
||||
|
||||
const field_name = try self.parseIdentifier();
|
||||
self.skipWhitespace();
|
||||
|
||||
if (self.pos >= self.content.len or self.content[self.pos] != '=')
|
||||
return error.InvalidSyntax;
|
||||
self.pos += 1;
|
||||
|
||||
self.skipWhitespace();
|
||||
|
||||
inline for (std.meta.fields(T)) |field| {
|
||||
if (std.mem.eql(u8, field_name, field.name)) {
|
||||
switch (@typeInfo(field.type)) {
|
||||
.bool => {
|
||||
const ident = try self.parseIdentifier();
|
||||
if (std.mem.eql(u8, ident, "true")) {
|
||||
@field(result, field.name) = true;
|
||||
} else if (std.mem.eql(u8, ident, "false")) {
|
||||
@field(result, field.name) = false;
|
||||
} else return error.InvalidSyntax;
|
||||
},
|
||||
.int => {
|
||||
@field(result, field.name) = @intCast(try self.parseNumber());
|
||||
},
|
||||
.array => |array_info| {
|
||||
const array = try self.parseArray();
|
||||
if (array.len > array_info.len) return error.InvalidSyntax;
|
||||
@field(result, field.name) = undefined;
|
||||
var dest = &@field(result, field.name);
|
||||
@memcpy(dest[0..array.len], array);
|
||||
},
|
||||
.@"enum" => {
|
||||
const ident = try self.parseIdentifier();
|
||||
inline for (std.meta.fields(field.type)) |enum_field| {
|
||||
if (std.mem.eql(u8, ident, enum_field.name)) {
|
||||
@field(result, field.name) = @field(field.type, enum_field.name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
.pointer => |ptr_info| {
|
||||
if (ptr_info.size != .Slice) return error.InvalidSyntax;
|
||||
switch (ptr_info.child) {
|
||||
u8 => {
|
||||
@field(result, field.name) = try self.parseString();
|
||||
},
|
||||
i64 => {
|
||||
@field(result, field.name) = try self.parseArray();
|
||||
},
|
||||
else => return error.InvalidSyntax,
|
||||
}
|
||||
},
|
||||
else => return error.InvalidSyntax,
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Example usage:
|
||||
const Config = struct {
|
||||
// Boolean tests
|
||||
bool_true: bool = false,
|
||||
bool_false: bool = true,
|
||||
|
||||
// Integer tests
|
||||
int_zero: i64 = 1,
|
||||
int_positive: i64 = 0,
|
||||
int_negative: i64 = 42,
|
||||
int_small: u32 = 16,
|
||||
|
||||
// Array tests
|
||||
array_empty: [4]i64 = .{ 0, 0, 0, 0 },
|
||||
array_full: [4]i64 = .{ 0, 1, 2, 3 },
|
||||
array_partial: [4]i64 = .{ 9, 8, 0, 0 },
|
||||
|
||||
// Enum tests
|
||||
enum_first: enum { First, Second, Third } = .Second,
|
||||
enum_last: enum { One, Two, Last } = .One,
|
||||
lscpu_core_strategy: enum { HighestFreq, Sequential } = .HighestFreq,
|
||||
|
||||
// String tests
|
||||
string_empty: []const u8 = "",
|
||||
string_simple: []const u8 = "hello",
|
||||
string_spaces: []const u8 = "hello world",
|
||||
string_special: []const u8 = "hello_123",
|
||||
};
|
||||
|
||||
test "parse config" {
|
||||
const content =
|
||||
\\bool_true = true
|
||||
\\bool_false = false
|
||||
\\int_zero = 0
|
||||
\\int_positive = 42
|
||||
\\int_negative = -123
|
||||
\\int_small = 16
|
||||
\\array_empty = []
|
||||
\\array_full = [0,1,2,3]
|
||||
\\array_partial = [9,8]
|
||||
\\enum_first = First
|
||||
\\enum_last = Last
|
||||
\\lscpu_core_strategy = HighestFreq
|
||||
\\string_empty = ""
|
||||
\\string_simple = "hello"
|
||||
\\string_spaces = "hello world"
|
||||
\\string_special = "hello_123"
|
||||
;
|
||||
|
||||
var parser = Parser(Config).init(std.heap.page_allocator, content);
|
||||
const config = try parser.parse();
|
||||
|
||||
// Boolean tests
|
||||
try std.testing.expect(config.bool_true);
|
||||
try std.testing.expect(!config.bool_false);
|
||||
|
||||
// Integer tests
|
||||
try std.testing.expectEqual(@as(i64, 0), config.int_zero);
|
||||
try std.testing.expectEqual(@as(i64, 42), config.int_positive);
|
||||
try std.testing.expectEqual(@as(i64, -123), config.int_negative);
|
||||
try std.testing.expectEqual(@as(u32, 16), config.int_small);
|
||||
|
||||
// Array tests
|
||||
try std.testing.expectEqualSlices(i64, &[_]i64{ 0, 0, 0, 0 }, &config.array_empty);
|
||||
try std.testing.expectEqualSlices(i64, &[_]i64{ 0, 1, 2, 3 }, &config.array_full);
|
||||
try std.testing.expectEqualSlices(i64, &[_]i64{ 9, 8, 0, 0 }, &config.array_partial);
|
||||
|
||||
// Enum tests
|
||||
try std.testing.expectEqual(@as(@TypeOf(config.enum_first), .First), config.enum_first);
|
||||
try std.testing.expectEqual(@as(@TypeOf(config.enum_last), .Last), config.enum_last);
|
||||
try std.testing.expectEqual(@as(@TypeOf(config.lscpu_core_strategy), .HighestFreq), config.lscpu_core_strategy);
|
||||
|
||||
// String tests
|
||||
try std.testing.expectEqualStrings("", config.string_empty);
|
||||
try std.testing.expectEqualStrings("hello", config.string_simple);
|
||||
try std.testing.expectEqualStrings("hello world", config.string_spaces);
|
||||
try std.testing.expectEqualStrings("hello_123", config.string_special);
|
||||
}
|
||||
|
||||
test "parse with missing fields" {
|
||||
const TestConfig = struct {
|
||||
name: []const u8 = "default",
|
||||
cores: []const i64 = &[_]i64{ 0, 1 },
|
||||
enabled: bool = true,
|
||||
count: u32 = 42,
|
||||
};
|
||||
|
||||
const content =
|
||||
\\name = "test"
|
||||
\\cores = [5,6,7]
|
||||
;
|
||||
|
||||
var parser = Parser(TestConfig).init(std.testing.allocator, content);
|
||||
const config = try parser.parse();
|
||||
defer {
|
||||
std.testing.allocator.free(config.name);
|
||||
std.testing.allocator.free(config.cores);
|
||||
}
|
||||
|
||||
try std.testing.expectEqualStrings("test", config.name);
|
||||
try std.testing.expectEqualSlices(i64, &[_]i64{ 5, 6, 7 }, config.cores);
|
||||
try std.testing.expect(config.enabled == true); // default value
|
||||
try std.testing.expect(config.count == 42); // default value
|
||||
}
|
126
falcond/src/power_profiles.zig
Normal file
126
falcond/src/power_profiles.zig
Normal file
@ -0,0 +1,126 @@
|
||||
const std = @import("std");
|
||||
const dbus = @import("dbus.zig");
|
||||
const Config = @import("config.zig").Config;
|
||||
|
||||
pub const PowerProfiles = struct {
|
||||
const PP_NAME = "org.freedesktop.UPower.PowerProfiles";
|
||||
const PP_PATH = "/org/freedesktop/UPower/PowerProfiles";
|
||||
const PP_IFACE = "org.freedesktop.UPower.PowerProfiles";
|
||||
|
||||
allocator: std.mem.Allocator,
|
||||
dbus: dbus.DBus,
|
||||
config: *Config,
|
||||
original_profile: ?[]const u8,
|
||||
has_performance: bool,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, config: *Config) !*PowerProfiles {
|
||||
var self = try allocator.create(PowerProfiles);
|
||||
errdefer allocator.destroy(self);
|
||||
|
||||
if (!config.enable_performance_mode) {
|
||||
std.log.info("Performance mode disabled in config", .{});
|
||||
self.* = .{
|
||||
.allocator = allocator,
|
||||
.dbus = undefined,
|
||||
.config = config,
|
||||
.original_profile = null,
|
||||
.has_performance = false,
|
||||
};
|
||||
return self;
|
||||
}
|
||||
|
||||
self.* = .{
|
||||
.allocator = allocator,
|
||||
.dbus = dbus.DBus.init(allocator, PP_NAME, PP_PATH, PP_IFACE),
|
||||
.config = config,
|
||||
.original_profile = null,
|
||||
.has_performance = false,
|
||||
};
|
||||
|
||||
const profiles = try self.getAvailableProfiles(allocator);
|
||||
defer {
|
||||
for (profiles) |profile| {
|
||||
allocator.free(profile);
|
||||
}
|
||||
allocator.free(profiles);
|
||||
}
|
||||
|
||||
std.log.info("Available power profiles:", .{});
|
||||
for (profiles) |profile| {
|
||||
std.log.info(" - {s}", .{profile});
|
||||
}
|
||||
|
||||
for (profiles) |profile| {
|
||||
if (std.mem.eql(u8, profile, "performance")) {
|
||||
self.has_performance = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *PowerProfiles) void {
|
||||
if (self.original_profile) |profile| {
|
||||
self.allocator.free(profile);
|
||||
}
|
||||
self.allocator.destroy(self);
|
||||
}
|
||||
|
||||
pub fn isPerformanceAvailable(self: *const PowerProfiles) bool {
|
||||
return self.has_performance;
|
||||
}
|
||||
|
||||
pub fn getAvailableProfiles(self: *PowerProfiles, alloc: std.mem.Allocator) ![]const []const u8 {
|
||||
var result = std.ArrayList([]const u8).init(alloc);
|
||||
errdefer {
|
||||
for (result.items) |item| {
|
||||
alloc.free(item);
|
||||
}
|
||||
result.deinit();
|
||||
}
|
||||
|
||||
const profiles_raw = try self.dbus.getPropertyArray("Profiles");
|
||||
defer {
|
||||
for (profiles_raw) |item| {
|
||||
alloc.free(item);
|
||||
}
|
||||
alloc.free(profiles_raw);
|
||||
}
|
||||
|
||||
var i: usize = 0;
|
||||
while (i < profiles_raw.len) : (i += 1) {
|
||||
const item = profiles_raw[i];
|
||||
if (std.mem.eql(u8, item, "Profile")) {
|
||||
if (i + 2 < profiles_raw.len) {
|
||||
try result.append(try alloc.dupe(u8, profiles_raw[i + 2]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result.toOwnedSlice();
|
||||
}
|
||||
|
||||
pub fn enablePerformanceMode(self: *PowerProfiles) !void {
|
||||
if (!self.has_performance) {
|
||||
std.log.warn("Performance mode not available", .{});
|
||||
return;
|
||||
}
|
||||
|
||||
if (self.original_profile == null) {
|
||||
self.original_profile = try self.dbus.getProperty("ActiveProfile");
|
||||
}
|
||||
|
||||
try self.dbus.setProperty("ActiveProfile", "performance");
|
||||
}
|
||||
|
||||
pub fn disablePerformanceMode(self: *PowerProfiles) !void {
|
||||
if (!self.has_performance) return;
|
||||
|
||||
if (self.original_profile) |profile| {
|
||||
try self.dbus.setProperty("ActiveProfile", profile);
|
||||
self.allocator.free(profile);
|
||||
self.original_profile = null;
|
||||
}
|
||||
}
|
||||
};
|
269
falcond/src/profile.zig
Normal file
269
falcond/src/profile.zig
Normal file
@ -0,0 +1,269 @@
|
||||
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 = .gaming,
|
||||
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",
|
||||
"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});
|
||||
try self.power_profiles.enablePerformanceMode();
|
||||
}
|
||||
|
||||
const effective_mode = if (self.config.vcache_mode != .none)
|
||||
self.config.vcache_mode
|
||||
else
|
||||
profile.vcache_mode;
|
||||
try vcache_setting.applyVCacheMode(effective_mode);
|
||||
|
||||
// Apply scheduler settings, using global config override if set
|
||||
const effective_sched = if (self.config.scx_sched != .none)
|
||||
self.config.scx_sched
|
||||
else
|
||||
profile.scx_sched;
|
||||
const effective_sched_mode = if (self.config.scx_sched != .none)
|
||||
self.config.scx_sched_props
|
||||
else
|
||||
profile.scx_sched_props;
|
||||
try scx_scheds.applyScheduler(self.allocator, effective_sched, effective_sched_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});
|
||||
try self.power_profiles.disablePerformanceMode();
|
||||
}
|
||||
|
||||
try vcache_setting.applyVCacheMode(.none);
|
||||
try 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() catch {};
|
||||
}
|
||||
}
|
||||
|
||||
for (self.profiles.items) |*profile| {
|
||||
self.allocator.free(profile.name);
|
||||
}
|
||||
self.queued_profiles.deinit();
|
||||
self.profiles.deinit();
|
||||
}
|
||||
};
|
224
falcond/src/scx_scheds.zig
Normal file
224
falcond/src/scx_scheds.zig
Normal file
@ -0,0 +1,224 @@
|
||||
const std = @import("std");
|
||||
const dbus = @import("dbus.zig");
|
||||
|
||||
pub const ScxError = dbus.DBusError;
|
||||
|
||||
pub const ScxScheduler = enum {
|
||||
bpfland,
|
||||
central,
|
||||
flash,
|
||||
flatcg,
|
||||
lavd,
|
||||
layered,
|
||||
nest,
|
||||
pair,
|
||||
qmap,
|
||||
rlfifo,
|
||||
rustland,
|
||||
rusty,
|
||||
sdt,
|
||||
simple,
|
||||
userland,
|
||||
vder,
|
||||
none,
|
||||
|
||||
pub fn toScxName(self: ScxScheduler) []const u8 {
|
||||
return switch (self) {
|
||||
.none => "",
|
||||
inline else => |tag| "scx_" ++ @tagName(tag),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn fromString(str: []const u8) ScxError!ScxScheduler {
|
||||
if (std.mem.eql(u8, str, "none")) return .none;
|
||||
if (std.mem.eql(u8, str, "scx_none")) return .none;
|
||||
if (std.mem.eql(u8, str, "unknown")) return .none;
|
||||
if (std.mem.eql(u8, str, "scx_bpfland")) return .bpfland;
|
||||
if (std.mem.eql(u8, str, "scx_central")) return .central;
|
||||
if (std.mem.eql(u8, str, "scx_flash")) return .flash;
|
||||
if (std.mem.eql(u8, str, "scx_flatcg")) return .flatcg;
|
||||
if (std.mem.eql(u8, str, "scx_lavd")) return .lavd;
|
||||
if (std.mem.eql(u8, str, "scx_layered")) return .layered;
|
||||
if (std.mem.eql(u8, str, "scx_nest")) return .nest;
|
||||
if (std.mem.eql(u8, str, "scx_pair")) return .pair;
|
||||
if (std.mem.eql(u8, str, "scx_qmap")) return .qmap;
|
||||
if (std.mem.eql(u8, str, "scx_rlfifo")) return .rlfifo;
|
||||
if (std.mem.eql(u8, str, "scx_rustland")) return .rustland;
|
||||
if (std.mem.eql(u8, str, "scx_rusty")) return .rusty;
|
||||
if (std.mem.eql(u8, str, "scx_sdt")) return .sdt;
|
||||
if (std.mem.eql(u8, str, "scx_simple")) return .simple;
|
||||
if (std.mem.eql(u8, str, "scx_userland")) return .userland;
|
||||
if (std.mem.eql(u8, str, "scx_vder")) return .vder;
|
||||
return error.InvalidValue;
|
||||
}
|
||||
};
|
||||
|
||||
pub const ScxSchedModes = enum {
|
||||
default,
|
||||
power,
|
||||
gaming,
|
||||
latency,
|
||||
server,
|
||||
};
|
||||
|
||||
const PreviousState = struct {
|
||||
scheduler: ?ScxScheduler = null,
|
||||
mode: ?ScxSchedModes = null,
|
||||
};
|
||||
|
||||
var previous_state: PreviousState = .{};
|
||||
var supported_schedulers: []ScxScheduler = undefined;
|
||||
var allocator: std.mem.Allocator = undefined;
|
||||
|
||||
pub fn init(alloc: std.mem.Allocator) !void {
|
||||
allocator = alloc;
|
||||
std.log.info("Initializing scheduler state", .{});
|
||||
|
||||
const sched_list = try getSupportedSchedulers(alloc);
|
||||
defer alloc.free(sched_list);
|
||||
|
||||
std.log.info("Supported schedulers:", .{});
|
||||
if (sched_list.len > 0) {
|
||||
for (sched_list) |sched| {
|
||||
std.log.info(" - {s}", .{sched.toScxName()});
|
||||
}
|
||||
supported_schedulers = try alloc.dupe(ScxScheduler, sched_list);
|
||||
} else {
|
||||
supported_schedulers = &[_]ScxScheduler{};
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deinit() void {
|
||||
if (supported_schedulers.len > 0) {
|
||||
allocator.free(supported_schedulers);
|
||||
}
|
||||
}
|
||||
|
||||
const SCX_NAME = "org.scx.Loader";
|
||||
const SCX_PATH = "/org/scx/Loader";
|
||||
const SCX_IFACE = "org.scx.Loader";
|
||||
|
||||
fn modeToInt(mode: ScxSchedModes) u32 {
|
||||
return switch (mode) {
|
||||
.default => 0,
|
||||
.power => 1,
|
||||
.gaming => 2,
|
||||
.latency => 3,
|
||||
.server => 4,
|
||||
};
|
||||
}
|
||||
|
||||
fn intToMode(value: u32) ScxError!ScxSchedModes {
|
||||
return switch (value) {
|
||||
0 => .default,
|
||||
1 => .power,
|
||||
2 => .gaming,
|
||||
3 => .latency,
|
||||
4 => .server,
|
||||
else => error.InvalidValue,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn getCurrentScheduler(alloc: std.mem.Allocator) !?ScxScheduler {
|
||||
var dbus_conn = dbus.DBus.init(alloc, SCX_NAME, SCX_PATH, SCX_IFACE);
|
||||
|
||||
const current = try dbus_conn.getProperty("CurrentScheduler");
|
||||
defer alloc.free(current);
|
||||
|
||||
if (current.len == 0) return null;
|
||||
return try ScxScheduler.fromString(current);
|
||||
}
|
||||
|
||||
pub fn getCurrentMode(alloc: std.mem.Allocator) !ScxSchedModes {
|
||||
var dbus_conn = dbus.DBus.init(alloc, SCX_NAME, SCX_PATH, SCX_IFACE);
|
||||
|
||||
const mode_str = try dbus_conn.getProperty("SchedulerMode");
|
||||
defer alloc.free(mode_str);
|
||||
|
||||
if (mode_str.len == 0) return .default;
|
||||
|
||||
const mode = try std.fmt.parseInt(u32, mode_str, 10);
|
||||
return intToMode(mode);
|
||||
}
|
||||
|
||||
pub fn getSupportedSchedulers(alloc: std.mem.Allocator) ![]ScxScheduler {
|
||||
var dbus_conn = dbus.DBus.init(alloc, SCX_NAME, SCX_PATH, SCX_IFACE);
|
||||
|
||||
const schedulers = try dbus_conn.getPropertyArray("SupportedSchedulers");
|
||||
defer {
|
||||
for (schedulers) |s| {
|
||||
alloc.free(s);
|
||||
}
|
||||
alloc.free(schedulers);
|
||||
}
|
||||
|
||||
var result = try std.ArrayList(ScxScheduler).initCapacity(alloc, schedulers.len);
|
||||
errdefer result.deinit();
|
||||
|
||||
for (schedulers) |s| {
|
||||
const scheduler = try ScxScheduler.fromString(s);
|
||||
try result.append(scheduler);
|
||||
}
|
||||
|
||||
return result.toOwnedSlice();
|
||||
}
|
||||
|
||||
pub fn storePreviousState(alloc: std.mem.Allocator) !void {
|
||||
std.log.info("Storing current scheduler state", .{});
|
||||
if (try getCurrentScheduler(alloc)) |scheduler| {
|
||||
if (scheduler == .none) {
|
||||
std.log.info("Current scheduler is none", .{});
|
||||
previous_state.scheduler = null;
|
||||
previous_state.mode = null;
|
||||
} else {
|
||||
std.log.info("Storing current scheduler: {s}", .{scheduler.toScxName()});
|
||||
previous_state.scheduler = scheduler;
|
||||
previous_state.mode = try getCurrentMode(alloc);
|
||||
}
|
||||
} else {
|
||||
std.log.info("No current scheduler", .{});
|
||||
previous_state.scheduler = null;
|
||||
previous_state.mode = null;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn activateScheduler(alloc: std.mem.Allocator, scheduler: ScxScheduler, mode: ScxSchedModes) ScxError!void {
|
||||
var dbus_conn = dbus.DBus.init(alloc, SCX_NAME, SCX_PATH, SCX_IFACE);
|
||||
|
||||
const mode_str = try std.fmt.allocPrint(alloc, "{d}", .{modeToInt(mode)});
|
||||
defer alloc.free(mode_str);
|
||||
|
||||
const args = [_][]const u8{
|
||||
"su",
|
||||
scheduler.toScxName(),
|
||||
mode_str,
|
||||
};
|
||||
|
||||
try dbus_conn.callMethod("SwitchScheduler", &args);
|
||||
}
|
||||
|
||||
pub fn applyScheduler(alloc: std.mem.Allocator, scheduler: ScxScheduler, mode: ScxSchedModes) ScxError!void {
|
||||
std.log.info("Applying scheduler {s} with mode {s}", .{ scheduler.toScxName(), @tagName(mode) });
|
||||
|
||||
try storePreviousState(alloc);
|
||||
try activateScheduler(alloc, scheduler, mode);
|
||||
}
|
||||
|
||||
pub fn deactivateScheduler(alloc: std.mem.Allocator) ScxError!void {
|
||||
var dbus_conn = dbus.DBus.init(alloc, SCX_NAME, SCX_PATH, SCX_IFACE);
|
||||
try dbus_conn.callMethod("StopScheduler", &[_][]const u8{});
|
||||
}
|
||||
|
||||
pub fn restorePreviousState(alloc: std.mem.Allocator) ScxError!void {
|
||||
if (previous_state.scheduler) |scheduler| {
|
||||
if (scheduler == .none) {
|
||||
std.log.info("Previous state was none, stopping scheduler", .{});
|
||||
try deactivateScheduler(alloc);
|
||||
} else {
|
||||
std.log.info("Restoring previous scheduler: {s}", .{scheduler.toScxName()});
|
||||
try activateScheduler(alloc, scheduler, previous_state.mode orelse .default);
|
||||
}
|
||||
previous_state.scheduler = null;
|
||||
previous_state.mode = null;
|
||||
}
|
||||
}
|
42
falcond/src/vcache_setting.zig
Normal file
42
falcond/src/vcache_setting.zig
Normal file
@ -0,0 +1,42 @@
|
||||
const std = @import("std");
|
||||
const fs = std.fs;
|
||||
|
||||
pub const VCacheMode = enum {
|
||||
cache,
|
||||
freq,
|
||||
none,
|
||||
};
|
||||
|
||||
const vcache_path = "/sys/bus/platform/drivers/amd_x3d_vcache/AMDI0101:00/amd_x3d_mode";
|
||||
|
||||
var previous_mode: ?[]const u8 = null;
|
||||
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,
|
||||
else => return err,
|
||||
};
|
||||
defer file.close();
|
||||
|
||||
if (vcache_mode == .none) {
|
||||
if (previous_mode) |mode| {
|
||||
try file.writeAll(mode);
|
||||
previous_mode = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const bytes_read = try file.readAll(previous_mode_buffer[0..]);
|
||||
if (bytes_read > 0) {
|
||||
previous_mode = previous_mode_buffer[0..bytes_read];
|
||||
}
|
||||
|
||||
try file.seekTo(0);
|
||||
|
||||
try file.writeAll(switch (vcache_mode) {
|
||||
.freq => "frequency",
|
||||
.cache => "cache",
|
||||
.none => unreachable,
|
||||
});
|
||||
}
|
23
main.sh
Executable file
23
main.sh
Executable file
@ -0,0 +1,23 @@
|
||||
#! /bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
VERSION="1.0.0"
|
||||
|
||||
source ./pika-build-config.sh
|
||||
|
||||
echo "$PIKA_BUILD_ARCH" > pika-build-arch
|
||||
|
||||
cd ./falcond
|
||||
|
||||
# Get build deps
|
||||
apt-get build-dep ./ -y
|
||||
|
||||
# Build package
|
||||
LOGNAME=root dh_make --createorig -y -l -p falcond_"$VERSION" || echo "dh-make: Ignoring Last Error"
|
||||
dpkg-buildpackage --no-sign
|
||||
|
||||
# Move the debs to output
|
||||
cd ../
|
||||
mkdir -p ./output
|
||||
mv ./*.deb ./output/
|
10
pika-build-config/amd64-v3.sh
Executable file
10
pika-build-config/amd64-v3.sh
Executable file
@ -0,0 +1,10 @@
|
||||
#! /bin/bash
|
||||
export PIKA_BUILD_ARCH="amd64-v3"
|
||||
export DEBIAN_FRONTEND="noninteractive"
|
||||
export DEB_BUILD_MAINT_OPTIONS="optimize=+lto -march=x86-64-v3 -O3 -flto=auto"
|
||||
export DEB_CFLAGS_MAINT_APPEND="-march=x86-64-v3 -O3 -flto=auto"
|
||||
export DEB_CPPFLAGS_MAINT_APPEND="-march=x86-64-v3 -O3 -flto=auto"
|
||||
export DEB_CXXFLAGS_MAINT_APPEND="-march=x86-64-v3 -O3 -flto=auto"
|
||||
export DEB_LDFLAGS_MAINT_APPEND="-march=x86-64-v3 -O3 -flto=auto"
|
||||
export DEB_BUILD_OPTIONS="nocheck notest terse"
|
||||
export DPKG_GENSYMBOLS_CHECK_LEVEL=0
|
5
pika-build-config/i386.sh
Executable file
5
pika-build-config/i386.sh
Executable file
@ -0,0 +1,5 @@
|
||||
#! /bin/bash
|
||||
export PIKA_BUILD_ARCH="i386"
|
||||
export DEBIAN_FRONTEND="noninteractive"
|
||||
export DEB_BUILD_OPTIONS="nocheck notest terse"
|
||||
export DPKG_GENSYMBOLS_CHECK_LEVEL=0
|
2
release.sh
Executable file
2
release.sh
Executable file
@ -0,0 +1,2 @@
|
||||
# send debs to server
|
||||
rsync -azP --include './' --include '*.deb' --exclude '*' ./output/ ferreo@direct.pika-os.com:/srv/www/cockatiel-incoming/
|
Loading…
x
Reference in New Issue
Block a user