diff --git a/.github/ISSUE_TEMPLATE/bug-report---packaging-windows.md b/.github/ISSUE_TEMPLATE/bug-report---packaging-windows.md new file mode 100644 index 0000000..fe63495 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report---packaging-windows.md @@ -0,0 +1,37 @@ +--- +name: Bug report | packaging/windows +about: Report a bug involving the windows packaging +title: "[BUG] [packaging/windows]" +labels: bug, packaging/windows +assignees: 'leopoldhub' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Error message** +```txt +If applicable, add the full error message. +``` + +**Environment (please complete the following information):** + - OS: [e.g. Windows 11] + - Version [e.g. commit 176d34b] + - Configuration file [config.json] + +**Additional context** +Add any other context about the problem here. diff --git a/.gitignore b/.gitignore index c28b5ba..7bb0dc6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# Nix Build dir +/result + ### Intellij template # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 diff --git a/README.md b/README.md index b7a4bf7..3bb9ca5 100644 --- a/README.md +++ b/README.md @@ -1,97 +1,126 @@ # fw-fanctrl -This is a simple Python service for Linux that drives Framework Laptop's fan(s) speed according to a configurable speed/temp curve. -Its default configuration targets very silent fan operation, but it's easy to configure it for a different comfort/performance trade-off. -Its possible to specify two separate fan curves depending on whether the Laptop is charging/discharging. -Under the hood, it uses [ectool](https://gitlab.howett.net/DHowett/ectool) to change parameters in Framework's embedded controller (EC). +[![Static Badge](https://img.shields.io/badge/Windows-0078D6?style=flat&label=Platform&link=https%3A%2F%2Fgithub.com%2FTamtamHero%2Ffw-fanctrl%2Ftree%2Fpackaging%2Fwindows)](https://github.com/TamtamHero/fw-fanctrl/tree/packaging/windows) -It is compatible with all kinds of 13" and 16" models, both AMD/Intel CPUs, with or without a discrete GPU. +[![Static Badge](https://img.shields.io/badge/Python__3.12-FFDE57?style=flat&label=Requirement&link=https%3A%2F%2Fwww.python.org%2Fdownloads)](https://www.python.org/downloads) + +## Additional platforms: + +[![Static Badge](https://img.shields.io/badge/Linux%2FGlobal-FCC624?style=flat&logo=linux&logoColor=FFFFFF&label=Platform&link=https%3A%2F%2Fgithub.com%2FTamtamHero%2Ffw-fanctrl%2Ftree%2Fmain)](https://github.com/TamtamHero/fw-fanctrl/tree/main) + +[![Static Badge](https://img.shields.io/badge/NixOS-5277C3?style=flat&logo=nixos&logoColor=FFFFFF&label=Platform&link=https%3A%2F%2Fgithub.com%2FTamtamHero%2Ffw-fanctrl%2Ftree%2Fpackaging%2Fnix)](https://github.com/TamtamHero/fw-fanctrl/tree/packaging/nix/doc/nix-flake.md) + +## Description + +Fw-fanctrl is a simple Python CLI service that controls Framework Laptop's fan(s) +speed according to a configurable speed/temperature curve. + +Its default strategy aims for very quiet fan operation, but you can choose amongst the other provided strategies, or +easily configure your own for a different comfort/performance trade-off. + +It also is possible to assign separate strategies depending on whether the laptop is charging or discharging. + +Under the hood, it uses [ectool](https://gitlab.howett.net/DHowett/ectool) +to change parameters in Framework's embedded controller (EC). + +It is compatible with all 13" and 16" models, both AMD/Intel CPUs, with or without a discrete GPU. If the service is paused or stopped, the fans will revert to their default behaviour. -# Install +## Table of Content -## Dependencies + +* [fw-fanctrl](#fw-fanctrl) + * [Additional platforms:](#additional-platforms) + * [Description](#description) + * [Table of Content](#table-of-content) + * [Documentation](#documentation) + * [Installation](#installation) + * [Other Platforms](#other-platforms) + * [Requirements](#requirements) + * [Dependencies](#dependencies) + * [Instructions](#instructions) + * [Update](#update) + * [Uninstall](#uninstall) + -To communicate with the embedded controller the `ectool` is required. -You can either let the script download it from the [gitlab repository](https://gitlab.howett.net/DHowett/ectool) artifacts, -or disable its installation (`--no-ectool`) and install your own. +## Documentation -You also need to disable secure boot of your device for `ectool` to work (more details about why [here](https://www.howett.net/posts/2021-12-framework-ec/#using-fw-ectool)) +More documentation could be found [here](./doc/README.md). -Then run: -```bash -sudo ./install.sh -``` +## Installation -This bash script will to create and activate a service that runs this repo's main script, `fanctrl.py`. -It will copy `fanctrl.py` (to an executable file `fw-fanctrl`), download the ectool to `[dest-dir(/)]/bin` and create a config file -in `[dest-dir(/)][sysconf-dir(/etc)]/fw-fanctrl/config.json` +### Other Platforms -this script also includes options to: -- specify an installation destination directory (`--dest-dir `). -- specify an installation prefix directory (`--prefix-dir `). -- specify a default configuration directory (`--sysconf-dir `). -- disable ectool installation and service activation (`--no-ectool`) -- disable post-install process (`--no-post-install`) -- disable pre-uninstall process (`--no-pre-uninstall`) +| name | branch | documentation | +|--------------|------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------| +| Linux/Global | [main](https://github.com/TamtamHero/fw-fanctrl/tree/main) | [main/doc](https://github.com/TamtamHero/fw-fanctrl/tree/main/doc/README.md) | +| NixOS | [packaging/nix](https://github.com/TamtamHero/fw-fanctrl/tree/packaging/nix) | [packaging/nix/doc/nix-flake](https://github.com/TamtamHero/fw-fanctrl/tree/packaging/nix/doc/nix-flake.md) | -# Update +### Requirements -To install an update, you can pull the latest commit on the `main` branch of this repository, and run the install script again. +| name | version | url | +|--------|---------|----------------------------------------------------------------------| +| Python | 3.12.x | [https://www.python.org/downloads](https://www.python.org/downloads) | -# Uninstall -```bash -sudo ./install.sh --remove -``` +### Dependencies -# Configuration +Dependencies are downloaded and installed automatically. -There is a single `config.json` file located at `[dest-dir(/)][sysconf-dir(/etc)]/fw-fanctrl/config.json`. +| name | version | url | +|----------------|--------------|------------------------------------------------------------------------------------------------------| +| DHowett@crosec | v0.0.2 | [https://github.com/DHowett/FrameworkWindowsUtils](https://github.com/DHowett/FrameworkWindowsUtils) | +| DHowett@ectool | artifact#904 | [https://gitlab.howett.net/DHowett/ectool](https://gitlab.howett.net/DHowett/ectool) | +| nssm | 2.24 | [https://nssm.cc](https://nssm.cc) | -(You will need to reload the configuration with) -```bash -fw-fanctrl --reload -``` +### Instructions + +Please note that the windows version of this service uses an unsigned +experimental [crosec](https://github.com/DHowett/FrameworkWindowsUtils) driver that may be unstable. +We are not responsible for any damage or data loss that this may cause. -It contains different strategies, ranked from the most silent to the noisiest. It is possible to specify two different strategies for charging/discharging allowing for different optimization goals. -On discharging one could have fan curve optimized for low fan speeds in order to save power while accepting a bit more heat. -On charging one could have a fan curve that focuses on keeping the CPU from throttling and the system cool, at the expense of fan noise. -You can add new strategies, and if you think you have one that deserves to be shared, feel free to make a PR to this repo :) +First, make sure that you have disabled secure boot in your BIOS/UEFI settings. +(more details on why [here](https://www.howett.net/posts/2021-12-framework-ec/#using-fw-ectool)) + +``` +============================================================================ +IF YOU HAVE BITLOCKER ENABLED, YOU WILL NEED YOUR RECOVERY CODE ON BOOT !!!! +PLEASE MAKE A BACKUP OF YOUR BITLOCKER RECOVERY KEY BEFORE YOU DO ANYTHING ! +YOU GET LOCKED OUT OF YOUR COMPUTER IF YOU ARE NOT CAREFUL ENOUGH ! +============================================================================ +``` -Strategies can be configured with the following parameters: +[Download the repo](https://github.com/TamtamHero/fw-fanctrl/archive/refs/heads/packaging/windows.zip) and extract it +manually, or download/clone it with the appropriate tools: -- **SpeedCurve**: +```shell +git clone --branch "packaging/windows" "https://github.com/TamtamHero/fw-fanctrl.git" +``` - This is the curve points for `f(temperature) = fan speed` +```shell +curl -L "https://github.com/TamtamHero/fw-fanctrl/archive/refs/heads/packaging/windows.zip" -o "./fw-fanctrl.zip" && tar -xf "./fw-fanctrl.zip" && del "./fw-fanctrl.zip" +``` - `fw-fanctrl` measures the CPU temperature, compute a moving average of it, and then find an appropriate `fan speed` value by interpolation on the curve. +Then run the installation script with administrator privileges (by double clicking it, or with the following command) -- **FanSpeedUpdateFrequency**: +```shell +install.bat +``` - Time interval between every update to the fan's speed. `fw-fanctrl` measures temperature every second and add it to its moving average, but the actual update to fan speed is controlled using this configuration. This is for comfort, otherwise the speed is changed too often and it is noticeable and annoying, especially at low speed. - For a more reactive fan, you can lower this setting. **Defaults to 5 seconds.** +You can add a number of arguments to the installation command to suit your needs -- **MovingAverageInterval**: +| argument | description | +|-----------------------|----------------------------------------------| +| `/no-battery-sensors` | disable checking battery temperature sensors | - Number of seconds on which the moving average of temperature is computed. Increase it, and the fan speed will change more gradually. Lower it, and it will gain in reactivity. **Defaults to 20 seconds.** +## Update -## Charging/Discharging strategies +To update, you can download or pull the appropriate branch from this repository, and run the installation script again. -The strategy active by default is the one specified in the `defaultStrategy` entry. Optionally a separate strategy only active during discharge can be defined, using the `strategyOnDischarging` entry. By default no extra strategy for discharging is provided, the default strategy is active during all times. +## Uninstall -# Commands +To uninstall, run the uninstallation script `uninstall.bat` (by double clicking it, or with the following command) -| Option | Context | Description | -|-----------------------------|-----------------|-------------------------------------------------------------------------------| -| \ | run & configure | the name of the strategy to use | -| --run | run | run the service | -| --config | run | specify the configuration path | -| --no-log | run | disable state logging | -| --query, -q | configure | print the current strategy name | -| --list-strategies | configure | print the available strategies | -| --reload, -r | configure | reload the configuration file | -| --pause | configure | temporarily disable the service and reset the fans to their default behaviour | -| --resume | configure | resume the service | -| --hardware-controller, --hc | run | select the hardware controller. choices: ectool | -| --socket-controller, --sc | run & configure | select the socket controller. choices: unix | +```shell +uninstall.bat +``` diff --git a/doc/README.md b/doc/README.md new file mode 100644 index 0000000..bb83560 --- /dev/null +++ b/doc/README.md @@ -0,0 +1,7 @@ +# Table of Content + +- [Windows Installation](../README.md#installation) +- [Linux/Global](https://github.com/TamtamHero/fw-fanctrl/tree/main#installation) +- [NixOS Flake](https://github.com/TamtamHero/fw-fanctrl/tree/packaging/nix/doc/nix-flake.md) +- [Commands](./commands.md) +- [Configuration](./configuration.md) diff --git a/doc/commands.md b/doc/commands.md new file mode 100644 index 0000000..a0be05d --- /dev/null +++ b/doc/commands.md @@ -0,0 +1,82 @@ +# Table of Content + + +* [Table of Content](#table-of-content) +* [Commands](#commands) + * [run](#run) + * [use](#use) + * [reset](#reset) + * [reload](#reload) + * [pause](#pause) + * [resume](#resume) + * [print](#print) + + +# Commands + +Here is a list of commands and options used to interact with the service. + +the base of all commands is the following + +```shell +fw-fanctrl [commands and options] +``` + +First, the global options + +| Option | Optional | Choices | Default | Description | +|---------------------------|----------|---------|---------|--------------------------------------------------------------------------------| +| --socket-controller, --sc | yes | win32 | win32 | the socket controller to use for communication between the cli and the service | + +## run + +run the service manually + +If you have installed it correctly, the systemd `fw-fanctrl.service` service will do this for you, so you probably will +never need those. + +| Option | Optional | Choices | Default | Description | +|-----------------------------|----------|----------------|----------------------|-----------------------------------------------------------------------------------| +| \ | yes | | the default strategy | the name of the strategy to use | +| --config | yes | \[CONFIG_PATH] | | the configuration file path | +| --silent, -s | yes | | | disable printing speed/temp status to stdout | +| --hardware-controller, --hc | yes | ectool | ectool | the hardware controller to use for fetching and setting the temp and fan(s) speed | +| --no-battery-sensors | yes | | | disable checking battery temperature sensors (for mainboards without batteries) | + +## use + +change the current strategy + +| Option | Optional | Description | +|-------------|----------|---------------------------------| +| \ | no | the name of the strategy to use | + +## reset + +reset to the default strategy + +## reload + +reload the configuration file + +## pause + +pause the service + +## resume + +resume the service + +## print + +print the selected information + +| Option | Optional | Choices | Default | Description | +|--------------------|----------|----------------------|---------|------------------------| +| \ | yes | current, list, speed | current | what should be printed | + +| Choice | Description | +|---------|----------------------------------| +| current | The current strategy being used | +| list | List available strategies | +| speed | The current fan speed percentage | diff --git a/doc/configuration.md b/doc/configuration.md new file mode 100644 index 0000000..241ef39 --- /dev/null +++ b/doc/configuration.md @@ -0,0 +1,103 @@ +# Table of Content + + +* [Table of Content](#table-of-content) +* [Configuration](#configuration) + * [Default strategy](#default-strategy) + * [Charging/Discharging strategies](#chargingdischarging-strategies) + * [Editing strategies](#editing-strategies) + + +# Configuration + +After installation, you will find the configuration file in the following location: + +`%Appdata%\fw-fanctrl\config.json` + +It contains a list of strategies, ranked from the quietest to loudest, as well as the default and discharging +strategies. + +For example, one could use a lower fan speed strategy on discharging to optimise battery life (- noise, + heat), +and a high fan speed strategy on AC (+ noise, - heat). + +You can add or edit strategies, and if you think you have one that deserves to be shared, feel free to make a PR to this +repo :) + +## Default strategy + +The default strategy is the one used when the service is started. + +It can be changed by replacing the value of the `defaultStrategy` field with one of the strategies present in the +configuration. + +```json +"defaultStrategy": "[STRATEGY NAME]" +``` + +## Charging/Discharging strategies + +The discharging strategy is the one that will be used when the laptop is not on AC, +Otherwise the default strategy is used. + +It can be changed by replacing the value of the `strategyOnDischarging` field with one of the strategies present in the +configuration. + +```json +"strategyOnDischarging": "[STRATEGY NAME]" +``` + +This is optional and can be left empty to have the same strategy at all times. + +## Editing strategies + +Strategies can be configured with the following parameters: + +> **SpeedCurve**: +> +> It is represented by the curve points for `f(temperature) = fan(s) speed`. +> +> ```json +> "speedCurve": [ +> { "temp": [TEMPERATURE POINT], "speed": [PERCENTAGE SPEED] }, +> ... +> ] +> ``` +> +> `fw-fanctrl` measures the CPU temperature, calculates a moving average of it, and then finds an +> appropriate `fan speed` +> value by interpolating on the curve. + +> **FanSpeedUpdateFrequency**: +> +> It is the interval in seconds between fan speed calculations. +> +> ```json +> "fanSpeedUpdateFrequency": [UPDATE FREQUENCY] +> ``` +> +> This is for comfort, otherwise the speed will change too often, which is noticeable and annoying, especially at low +> speed. +> +> For a more responsive fan, you can reduce this setting. +> +> **Defaults to 5 seconds.** (minimum 1) + +> **MovingAverageInterval**: +> +> It is the number of seconds over which the moving average of temperature is calculated. +> +> ```json +> "movingAverageInterval": [AVERAGING INTERVAL] +> ``` +> +> Increase it, and the fan speed changes more gradually. Lower it, and it becomes more responsive. +> +> **Defaults to 20 seconds.** (minimum 1) + +--- + +Once the configuration has been changed, you must reload it with the following command + +```bash +fw-fanctrl reload +``` diff --git a/fanctrl.py b/fanctrl.py index 9ac7bd4..00e7fd0 100644 --- a/fanctrl.py +++ b/fanctrl.py @@ -1,22 +1,133 @@ #! /usr/bin/python3 import argparse import collections +import io import json -import os import re import shlex -import socket import subprocess import sys import threading +import traceback +import textwrap from time import sleep from abc import ABC, abstractmethod -DEFAULT_CONFIGURATION_FILE_PATH = "/etc/fw-fanctrl/config.json" -SOCKETS_FOLDER_PATH = "/run/fw-fanctrl" -COMMANDS_SOCKET_FILE_PATH = os.path.join(SOCKETS_FOLDER_PATH, ".fw-fanctrl.commands.sock") +DEFAULT_CONFIGURATION_FILE_PATH = "%%appdata%%/fw-fanctrl/config.json" +WINDOWS_SOCKET_PATH = r"\\.\pipe\fw-fanctrl.socket" -parser = None + +class CommandParser: + isRemote = True + + legacyParser = None + parser = None + + def __init__(self, isRemote=False): + self.isRemote = isRemote + self.initParser() + + def initParser(self): + self.parser = argparse.ArgumentParser( + prog="fw-fanctrl", + description="control Framework's laptop fan(s) with a speed curve", + epilog=textwrap.dedent( + "obtain more help about a command or subcommand using `fw-fanctrl [subcommand...] -h/--help`"), + formatter_class=argparse.RawTextHelpFormatter + ) + self.parser.add_argument( + "--socket-controller", + "--sc", + help="the socket controller to use for communication between the cli and the service", + type=str, + choices=["win32"], + default="win32" + ) + + commandsSubParser = self.parser.add_subparsers(dest="command") + + if not self.isRemote: + runCommand = commandsSubParser.add_parser( + "run", + description="run the service", + formatter_class=argparse.RawTextHelpFormatter + ) + runCommand.add_argument( + "strategy", + help='name of the strategy to use e.g: "lazy" (use `print strategies` to list available strategies)', + nargs=argparse.OPTIONAL + ) + runCommand.add_argument( + "--config", + "-c", + help=f"the configuration file path (default: {DEFAULT_CONFIGURATION_FILE_PATH})", + type=str, + default=DEFAULT_CONFIGURATION_FILE_PATH + ) + runCommand.add_argument( + "--silent", + "-s", + help="disable printing speed/temp status to stdout", + action="store_true" + ) + runCommand.add_argument( + "--hardware-controller", + "--hc", + help="the hardware controller to use for fetching and setting the temp and fan(s) speed", + type=str, + choices=["ectool"], + default="ectool" + ) + runCommand.add_argument( + "--no-battery-sensors", + help="disable checking battery temperature sensors", + action="store_true", + ) + + useCommand = commandsSubParser.add_parser( + "use", + description="change the current strategy" + ) + useCommand.add_argument( + "strategy", + help='name of the strategy to use e.g: "lazy". (use `print strategies` to list available strategies)' + ) + + commandsSubParser.add_parser( + "reset", + description="reset to the default strategy" + ) + commandsSubParser.add_parser( + "reload", + description="reload the configuration file" + ) + commandsSubParser.add_parser( + "pause", + description="pause the service" + ) + commandsSubParser.add_parser( + "resume", + description="resume the service" + ) + + printCommand = commandsSubParser.add_parser( + "print", + description="print the selected information", + formatter_class=argparse.RawTextHelpFormatter + ) + printCommand.add_argument( + "print_selection", + help="current - The current strategy\nlist - List available strategies\nspeed - The current fan speed percentage", + nargs="?", + type=str, + choices=["current", + "list", + "speed"], + default="current" + ) + + def parseArgs(self, args=None): + return self.parser.parse_args(args) class JSONException(Exception): @@ -54,7 +165,7 @@ def __init__(self, name, parameters): class Configuration: path = None - data: None + data = None def __init__(self, path): self.path = path @@ -107,67 +218,137 @@ def sendViaClientSocket(self, command): raise UnimplementedException() -class UnixSocketController(SocketController, ABC): +class WindowsSocketController(SocketController, ABC): + import ctypes + from ctypes import wintypes, windll + + _ctypes = ctypes + + CreateNamedPipe = windll.kernel32.CreateNamedPipeW + ConnectNamedPipe = windll.kernel32.ConnectNamedPipe + DisconnectNamedPipe = windll.kernel32.DisconnectNamedPipe + CreateFile = windll.kernel32.CreateFileW + ReadFile = windll.kernel32.ReadFile + WriteFile = windll.kernel32.WriteFile + CloseHandle = windll.kernel32.CloseHandle + + LPSECURITY_ATTRIBUTES = ctypes.c_void_p + LPDWORD = ctypes.POINTER(wintypes.DWORD) + LPVOID = ctypes.c_void_p + DWORD = wintypes.DWORD + + kernel32 = ctypes.WinDLL("kernel32", use_last_error=True) + advapi32 = ctypes.WinDLL("advapi32", use_last_error=True) + + ConvertStringSecurityDescriptorToSecurityDescriptorW = advapi32.ConvertStringSecurityDescriptorToSecurityDescriptorW + ConvertStringSecurityDescriptorToSecurityDescriptorW.argtypes = [wintypes.LPCWSTR, wintypes.DWORD, ctypes.POINTER(wintypes.LPVOID), ctypes.POINTER(wintypes.DWORD)] + ConvertStringSecurityDescriptorToSecurityDescriptorW.restype = wintypes.HANDLE + + sddl_string = "D:P(A;;GA;;;WD)" + + security_descriptor = wintypes.LPVOID() + ConvertStringSecurityDescriptorToSecurityDescriptorW(sddl_string, 1, ctypes.byref(security_descriptor), None) + + class SECURITY_ATTRIBUTES(ctypes.Structure): + from ctypes import wintypes + + _fields_ = [ + ("nLength", wintypes.DWORD), + ("lpSecurityDescriptor", wintypes.LPVOID), + ("nLength", wintypes.BOOL), + ] + + security_attributes = SECURITY_ATTRIBUTES() + security_attributes.nLength = ctypes.sizeof(SECURITY_ATTRIBUTES) + security_attributes.lpSecurityDescriptor = security_descriptor + security_attributes.bInheritHandle = False + server_socket = None def startServerSocket(self, commandCallback=None): if self.server_socket: raise SocketAlreadyRunningException(self.server_socket) - self.server_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - if os.path.exists(COMMANDS_SOCKET_FILE_PATH): - os.remove(COMMANDS_SOCKET_FILE_PATH) + self.server_socket = self.CreateNamedPipe( + WINDOWS_SOCKET_PATH, + self.ctypes.wintypes.DWORD(3), # PIPE_ACCESS_DUPLEX + self.ctypes.wintypes.DWORD(4), # PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT + 1, # nMaxInstances + 65536, # nOutBufferSize + 65536, # nInBufferSize + 0, # nDefaultTimeOut + self.ctypes.byref(self.security_attributes) # lpSecurityAttributes + ) try: - if not os.path.exists(SOCKETS_FOLDER_PATH): - os.makedirs(SOCKETS_FOLDER_PATH) - self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - self.server_socket.bind(COMMANDS_SOCKET_FILE_PATH) - os.chmod(COMMANDS_SOCKET_FILE_PATH, 0o777) - self.server_socket.listen(1) while True: - client_socket, _ = self.server_socket.accept() + self.ConnectNamedPipe(self.server_socket, None) + parsePrintCapture = io.StringIO() try: # Receive data from the client - data = client_socket.recv(4096).decode() - args = parser.parse_args(shlex.split(data)) + buffer = self.ctypes.create_string_buffer(65536) + bytes_read = self.ctypes.wintypes.DWORD(0) + self.ReadFile(self.server_socket, buffer, 65536, self.ctypes.byref(bytes_read), None) + data = buffer.raw[:bytes_read.value].decode('utf-8') + + original_stderr = sys.stderr + original_stdout = sys.stdout + # capture parsing std outputs for the client + sys.stderr = parsePrintCapture + sys.stdout = parsePrintCapture + try: + args = CommandParser(True).parseArgs(shlex.split(data)) + finally: + sys.stderr = original_stderr + sys.stdout = original_stdout commandReturn = commandCallback(args) if not commandReturn: commandReturn = "Success!" - client_socket.sendall(commandReturn.encode('utf-8')) + if parsePrintCapture.getvalue().strip(): + commandReturn = parsePrintCapture.getvalue() + commandReturn + commandReturn = commandReturn.encode('utf-8') + bytes_written = self.ctypes.wintypes.DWORD(0) + self.WriteFile(self.server_socket, commandReturn, len(commandReturn), + self.ctypes.byref(bytes_written), None) except Exception as e: - client_socket.sendall(f"[Error] > An error occurred: {e}".encode('utf-8')) + print(f"[Error] > An error occurred while treating a socket command: {e}", file=sys.stderr) + message = f"[Error] > An error occurred: {e}".encode('utf-8') + bytes_written = self.wintypes.DWORD(0) + self.WriteFile(self.server_socket, message, len(message), self.ctypes.byref(bytes_written), None) finally: - client_socket.shutdown(socket.SHUT_WR) - client_socket.close() + self.DisconnectNamedPipe(self.server_socket) finally: self.stopServerSocket() def stopServerSocket(self): if self.server_socket: - self.server_socket.close() + self.CloseHandle(self.server_socket) self.server_socket = None def isServerSocketRunning(self): return self.server_socket is not None def sendViaClientSocket(self, command): - client_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + client_socket = self.CreateFile( + WINDOWS_SOCKET_PATH, + self.ctypes.wintypes.DWORD(0x00000003), # GENERIC_READ | GENERIC_WRITE + 0, + None, + self.ctypes.wintypes.DWORD(3), # OPEN_EXISTING + 0, + None + ) try: - client_socket.connect(COMMANDS_SOCKET_FILE_PATH) - client_socket.sendall(command.encode('utf-8')) - received_data = b"" - while True: - data_chunk = client_socket.recv(1024) - if not data_chunk: - break - received_data += data_chunk - # Receive data from the server - data = received_data.decode() + message = command.encode('utf-8') + bytes_written = self.ctypes.wintypes.DWORD(0) + self.WriteFile(client_socket, message, len(message), self.ctypes.byref(bytes_written), None) + buffer = self.ctypes.create_string_buffer(65536) + bytes_read = self.ctypes.wintypes.DWORD(0) + self.ReadFile(client_socket, buffer, 65536, self.ctypes.byref(bytes_read), None) + data = buffer.raw[:bytes_read.value].decode('utf-8') if data.startswith("[Error] > "): raise Exception(data) return data finally: - if client_socket: - client_socket.close() + self.CloseHandle(client_socket) class HardwareController(ABC): @@ -193,9 +374,31 @@ def isOnAC(self): class EctoolHardwareController(HardwareController, ABC): + noBatterySensorMode = False + nonBatterySensors = None + + def __init__(self, noBatterySensorMode=False): + if noBatterySensorMode: + self.noBatterySensorMode = True + self.populateNonBatterySensors() + + def populateNonBatterySensors(self): + self.nonBatterySensors = [] + rawOut = subprocess.run("ectool tempsinfo all", stdout=subprocess.PIPE, shell=True, text=True).stdout + batterySensorsRaw = re.findall(r"\d+ Battery", rawOut, re.MULTILINE) + batterySensors = [x.split(" ")[0] for x in batterySensorsRaw] + for x in re.findall(r"^\d+", rawOut, re.MULTILINE): + if x not in batterySensors: + self.nonBatterySensors.append(x) def getTemperature(self): - rawOut = subprocess.run("ectool temps all", stdout=subprocess.PIPE, shell=True, text=True).stdout + if self.noBatterySensorMode: + rawOut = "".join([ + subprocess.run("ectool temps " + x, stdout=subprocess.PIPE, shell=True, text=True).stdout + for x in self.nonBatterySensors + ]) + else: + rawOut = subprocess.run("ectool temps all", stdout=subprocess.PIPE, shell=True, text=True).stdout rawTemps = re.findall(r'\(= (\d+) C\)', rawOut) temps = sorted([x for x in [int(x) for x in rawTemps] if x > 0], reverse=True) # safety fallback to avoid damaging hardware @@ -273,33 +476,37 @@ def getCurrentStrategy(self): return self.configuration.getDefaultStrategy() return self.configuration.getDischargingStrategy() - def commandManager(self, command): - if command.strategy or command._strategy: - strategy = command.strategy - if strategy is None: - strategy = command._strategy + def commandManager(self, args): + if args.command == "reset" or (args.command == "use" and args.strategy == "defaultStrategy"): + self.clearOverwrittenStrategy() + return + elif args.command == "use": try: - if strategy == "defaultStrategy": - self.clearOverwrittenStrategy() - else: - self.overwriteStrategy(strategy) + self.overwriteStrategy(args.strategy) return self.getCurrentStrategy().name except InvalidStrategyException: - raise InvalidStrategyException(f"The specified strategy is invalid: {strategy}") - elif command.pause: - self.pause() - elif command.resume: - self.resume() - elif command.query: - return self.getCurrentStrategy().name - elif command.list_strategies: - return '\n'.join(self.configuration.getStrategies()) - elif command.reload: + raise InvalidStrategyException(f"The specified strategy is invalid: {args.strategy}") + elif args.command == "reload": if self.configuration.reload(): if self.overwrittenStrategy is not None: self.overwriteStrategy(self.overwrittenStrategy.name) else: raise JSONException("Config file could not be parsed due to JSON Error") + return + elif args.command == "pause": + self.pause() + return + elif args.command == "resume": + self.resume() + return + elif args.command == "print": + if args.print_selection == "current": + return self.getCurrentStrategy().name + elif args.print_selection == "list": + return '\n'.join(self.configuration.getStrategies()) + elif args.print_selection == "speed": + return str(self.speed) + '%' + return "Unknown command, unexpected." # return mean temperature over a given time interval (in seconds) def getMovingAverageTemperature(self, timeInterval): @@ -360,70 +567,28 @@ def run(self, debug=True): else: sleep(5) except InvalidStrategyException as e: - print("Error: missing strategy, exiting for safety reasons: " + e.args[0]) - exit(1) + print(f"[Error] > Missing strategy, exiting for safety reasons: {e.args[0]}", file=sys.stderr) + except Exception as e: + print(f"[Error] > Critical error, exiting for safety reasons: {e}", file=sys.stderr) + traceback.print_exc() + exit(1) def main(): - global parser - parser = argparse.ArgumentParser( - description="Control Framework's laptop fan with a speed curve", - ) - - bothGroup = parser.add_argument_group("both") - bothGroup.add_argument( - "_strategy", - nargs="?", - help='Name of the strategy to use e.g: "lazy" (check config.json for others). Use "defaultStrategy" to go ' - 'back to the default strategy', - ) - bothGroup.add_argument( - "--strategy", - nargs="?", - help='Name of the strategy to use e.g: "lazy" (check config.json for others). Use "defaultStrategy" to go ' - 'back to the default strategy', - ) - - runGroup = parser.add_argument_group("run") - runGroup.add_argument("--run", help="run the service", action="store_true") - runGroup.add_argument("--config", type=str, help="Path to config file", default=DEFAULT_CONFIGURATION_FILE_PATH) - runGroup.add_argument( - "--no-log", help="Disable print speed/meanTemp to stdout", action="store_true" - ) - commandGroup = parser.add_argument_group("configure") - commandGroup.add_argument( - "--query", "-q", help="Query the currently active strategy", action="store_true" - ) - commandGroup.add_argument( - "--list-strategies", help="List the available strategies", action="store_true" - ) - commandGroup.add_argument( - "--reload", "-r", help="Reload the configuration from file", action="store_true" - ) - commandGroup.add_argument("--pause", help="Pause the program", action="store_true") - commandGroup.add_argument("--resume", help="Resume the program", action="store_true") - commandGroup.add_argument("--hardware-controller", "--hc", help="Select the hardware controller", type=str, - choices=["ectool"], default="ectool") - commandGroup.add_argument("--socket-controller", "--sc", help="Select the socket controller", type=str, - choices=["unix"], default="unix") - - args = parser.parse_args() + args = CommandParser().parseArgs() - socketController = None - if args.socket_controller == "unix": - socketController = UnixSocketController() + socketController = WindowsSocketController() + if args.socket_controller == "win32": + socketController = WindowsSocketController() - if args.run: - hardwareController = None + if args.command == "run": + hardwareController = EctoolHardwareController(noBatterySensorMode=args.no_battery_sensors) if args.hardware_controller == "ectool": - hardwareController = EctoolHardwareController() + hardwareController = EctoolHardwareController(noBatterySensorMode=args.no_battery_sensors) - strategy = args.strategy - if strategy is None: - strategy = args._strategy fan = FanController(hardwareController=hardwareController, socketController=socketController, configPath=args.config, strategyName=args.strategy) - fan.run(debug=not args.no_log) + fan.run(debug=not args.silent) else: try: commandResult = socketController.sendViaClientSocket(' '.join(sys.argv[1:])) diff --git a/fetch/ectool/linux/gitlab_job_id b/fetch/ectool/linux/gitlab_job_id deleted file mode 100644 index 2a0f68f..0000000 --- a/fetch/ectool/linux/gitlab_job_id +++ /dev/null @@ -1 +0,0 @@ -899 \ No newline at end of file diff --git a/fetch/ectool/linux/hash.sha256 b/fetch/ectool/linux/hash.sha256 deleted file mode 100644 index a926f8b..0000000 --- a/fetch/ectool/linux/hash.sha256 +++ /dev/null @@ -1 +0,0 @@ -ab94a1e9a33f592d5482dbfd4f42ad351ef91227ee3b3707333c0107d7f2b1b0 \ No newline at end of file diff --git a/install.bat b/install.bat new file mode 100644 index 0000000..e8c380a --- /dev/null +++ b/install.bat @@ -0,0 +1,530 @@ +@echo off + +setlocal + +for /F "delims=#" %%E in ('"prompt #$E# & for %%E in (1) do rem"') do set "ESC=%%E" + +:: Check if the script is running with administrative privileges +net session > nul 2>&1 +:: If not, relaunch the script with elevated privileges +if %errorLevel% neq 0 ( + echo requesting administrative privileges... + :: Check if there are arguments + if "%~1"=="" ( + PowerShell -Command "Start-Process '%~f0' -Verb runAs" + ) else ( + PowerShell -Command "Start-Process '%~f0' -ArgumentList '%*' -Verb runAs" + ) + exit /b +) + +echo running with administrative privileges... + +set "ARG_remove=" +set "ARG_r=" +set "ARG_no-battery-sensor=" + +CALL :ARG-PARSER %* + +if defined ARG_r ( + if not defined ARG_remove set "ARG_remove=%ARG_r%" +) + +cd /d "%~dp0" + +if not defined ARG_remove ( + GOTO :ACKNOWLEDGEMENT-CHECK +) + +if defined ARG_remove ( + echo: + echo ---------- + CALL :UNINSTALL +) + +GOTO :EOF + +:ACKNOWLEDGEMENT-CHECK + set "acknowledgementPhrase=this is dangerous and I know what I am doing" + echo %ESC%[91m + echo ====================================== WARNING ====================================== + echo 1. THIS WINDOWS VERSION REQUIRES THE USE OF AN UNSIGNED 'crosec' DRIVER TO WORK. + echo 2. SECURE BOOT MUST BE DISABLED IN ORDER TO USE THE PROGRAM + echo 3. IF YOU HAVE BITLOCKER ENABLED, YOU WILL NEED YOUR RECOVERY CODE ON BOOT !!!! + echo ====================================== WARNING ====================================== + echo PLEASE MAKE A BACKUP OF YOUR BITLOCKER RECOVERY KEY BEFORE YOU DO ANYTHING ! + echo YOU GET LOCKED OUT OF YOUR COMPUTER IF YOU ARE NOT CAREFUL ENOUGH ! + echo PROCEED WITH THE INSTALLATION IF YOU ARE ABSOLUTELY SURE OF WHAT YOU ARE DOING ! + echo ------------------------------------------------------------------------------------- + echo %ESC%[0m + echo to continue the installation, type '%acknowledgementPhrase%'. + echo to stop here, simply press enter. + set "acknowledgement=" + set /p "acknowledgement=> " + if not defined acknowledgement ( + echo goodbye. + pause + exit /b 2 + ) + if not "%acknowledgement%" equ "%acknowledgementPhrase%" ( + echo wrong acknowledgement phrase [%acknowledgement%] not equal to [%acknowledgementPhrase%], stopping here! + pause + exit /b 2 + ) + echo: + echo ---------- + CALL :INSTALL + GOTO :EOF +GOTO :EOF + +:INSTALL + CALL :UNINSTALL + + "%localAppData%\Programs\Python\Python312\python" --version + if %errorLevel% neq 0 ( + echo python 3.12.x is required to use 'fw-fanctrl' + echo please install it from the official website https://www.python.org/ before running this script + GOTO :EOF + ) + + echo installing + + set "addedEnvironmentPaths=" + + echo: + echo ---------- + CALL :install-crosec + if %errorLevel% neq 0 ( + echo failed to install 'crosec' + GOTO UNINSTALL + ) + + echo: + echo ---------- + CALL :install-ectool + if %errorLevel% neq 0 ( + echo failed to install 'ectool' + GOTO UNINSTALL + ) + + echo: + echo ---------- + CALL :install-nssm + if %errorLevel% neq 0 ( + echo failed to install 'nssm' + GOTO UNINSTALL + ) + + echo: + echo ---------- + CALL :install-fw-fanctrl + if %errorLevel% neq 0 ( + echo failed to install 'fw-fanctrl' + GOTO UNINSTALL + ) + + if defined addedEnvironmentPaths ( + echo adding '%addedEnvironmentPaths%' to path + @echo on + powershell -Command "[System.Environment]::SetEnvironmentVariable('Path', $env:Path+'%addedEnvironmentPaths%', [System.EnvironmentVariableTarget]::Machine)" + powershell -Command "[System.Environment]::SetEnvironmentVariable('Path', [System.Environment]::GetEnvironmentVariable('Path', [System.EnvironmentVariableTarget]::Machine) + ';' + [System.Environment]::GetEnvironmentVariable('Path', [System.EnvironmentVariableTarget]::User), [System.EnvironmentVariableTarget]::Process)" + @echo off + ) + + echo starting 'fw-fanctrl' service + @echo on + "%ProgramFiles%\nssm\nssm" start "fw-fanctrl" + "%ProgramFiles%\nssm\nssm" continue "fw-fanctrl" + @echo off + if %errorLevel% neq 0 ( + echo failed to start 'fw-fanctrl' service + ) + + rmdir /s /q ".temp" 2> nul + + pause + GOTO :EOF +GOTO :EOF + +:install-crosec + echo setting up 'crosec' + rmdir /s /q ".temp" 2> nul + mkdir ".temp" + + echo enabling 'bcdedit testsigning' + bcdedit /set {default} testsigning on + + echo downloading 'crosec.zip' + @echo on + curl -s -o ".temp\crosec.zip" -L "https://github.com/DHowett/FrameworkWindowsUtils/releases/download/v0.0.2/CrosEC-0.0.2-4ac038b.zip" > nul + @echo off + if %errorLevel% neq 0 ( + echo failed to download 'crosec.zip' + pause + exit /b 1 + ) + + echo extracting 'crosec.zip' + @echo on + tar -xf ".temp\crosec.zip" --strip-components=1 -C ".temp" > nul + @echo off + if %errorLevel% neq 0 ( + echo failed to extract 'crosec.zip' + pause + exit /b 2 + ) + + echo installing 'crosec' driver + + cd /d ".temp" + @echo on + ".\installer" install + @echo off + if %errorLevel% neq 0 ( + cd /d "%~dp0" + echo failed to run the 'crosec' driver installation + pause + exit /b 3 + ) + cd /d "%~dp0" + + echo testing 'crosec' driver + @echo on + ".temp\fauxectool" > ".temp\test-result.txt" + @echo off + + set count=0 + for %%i in (".temp\test-result.txt") do @set count=%%~zi + if "%count%" == "0" ( + echo 'crosec' driver not installed correctly + pause + exit /b 4 + ) + + rmdir /s /q "%ProgramFiles%\crosec" 2> nul + echo copying '.temp' to '%ProgramFiles%\crosec' + @echo on + xcopy /e /i ".temp" "%ProgramFiles%\crosec" > nul + @echo off + if %errorLevel% neq 0 ( + echo unable to copy '.temp' to '%ProgramFiles%\ectool' + pause + exit /b 5 + ) + + GOTO :EOF +GOTO :EOF + +:install-ectool + echo setting up 'ectool' + rmdir /s /q ".temp" 2> nul + mkdir ".temp" + + echo downloading 'artifact.zip' + @echo on + curl -s -o ".temp\artifact.zip" -L "https://gitlab.howett.net/DHowett/ectool/-/jobs/904/artifacts/download?file_type=archive" > nul + @echo off + if %errorLevel% neq 0 ( + echo failed to download 'artifact.zip' + pause + exit /b 1 + ) + + echo extracting 'artifact.zip' + @echo on + tar -xf ".temp\artifact.zip" --strip-components=3 -C ".temp" > nul + @echo off + if %errorLevel% neq 0 ( + echo failed to extract 'artifact.zip' + pause + exit /b 2 + ) + + rmdir /s /q "%ProgramFiles%\ectool" 2> nul + echo creating directory '%ProgramFiles%\ectool' + @echo on + mkdir "%ProgramFiles%\ectool" > nul + @echo off + if %errorLevel% neq 0 ( + echo unable to create directory '%ProgramFiles%\ectool' + pause + exit /b 3 + ) + + @echo %PATH% | findstr /I /C:"%ProgramFiles%\ectool" >nul + if %errorLevel% neq 0 ( + set "addedEnvironmentPaths=%addedEnvironmentPaths%;%ProgramFiles%\ectool" + ) + + echo installing 'ectool.exe' to '%ProgramFiles%\ectool' + @echo on + copy /v ".temp\ectool.exe" "%ProgramFiles%\ectool" > nul + @echo off + if %errorLevel% neq 0 ( + echo unable to install 'ectool.exe' + pause + exit /b 3 + ) + + GOTO :EOF +GOTO :EOF + +:install-nssm + echo setting up 'nssm' + rmdir /s /q ".temp" 2> nul + mkdir ".temp" + + echo downloading 'nssm.zip' + @echo on + curl -s -o ".temp\nssm.zip" -L "https://nssm.cc/release/nssm-2.24.zip" > nul + @echo off + if %errorLevel% neq 0 ( + echo failed to download 'nssm.zip' + pause + exit /b 1 + ) + + echo extracting 'nssm.zip' + @echo on + tar -xf ".temp\nssm.zip" --strip-components=1 -C ".temp" > nul + @echo off + if %errorLevel% neq 0 ( + echo failed to extract 'nssm.zip' + pause + exit /b 2 + ) + + echo creating directory '%ProgramFiles%\nssm' + @echo on + mkdir "%ProgramFiles%\nssm" > nul 2> nul + @echo off + + echo installing 'nssm.exe' to '%ProgramFiles%\nssm\' + @echo on + copy /v ".temp\win64\nssm.exe" "%ProgramFiles%\nssm\" > nul + @echo off + if %errorLevel% neq 0 ( + echo unable to install 'nssm.exe' + pause + exit /b 3 + ) + + GOTO :EOF +GOTO :EOF + +:install-fw-fanctrl + echo setting up 'fw-fanctrl' + rmdir /s /q "%ProgramFiles%\fw-fanctrl" 2> nul + echo creating directory '%ProgramFiles%\fw-fanctrl' + @echo on + mkdir "%ProgramFiles%\fw-fanctrl" > nul + @echo off + if %errorLevel% neq 0 ( + echo unable to create directory '%ProgramFiles%\fw-fanctrl' + pause + exit /b 3 + ) + + @echo %PATH% | findstr /I /C:"%ProgramFiles%\fw-fanctrl" >nul + if %errorLevel% neq 0 ( + set "addedEnvironmentPaths=%addedEnvironmentPaths%;%ProgramFiles%\fw-fanctrl" + ) + + echo installing 'fanctrl.py' to '%ProgramFiles%\fw-fanctrl' + @echo on + copy /v ".\fanctrl.py" "%ProgramFiles%\fw-fanctrl" > nul + @echo off + if %errorLevel% neq 0 ( + echo unable to install 'fanctrl.py' + pause + exit /b 3 + ) + + echo installing '.\services\windows\run-service.bat' to '%ProgramFiles%\fw-fanctrl' + @echo on + copy /v ".\services\windows\run-service.bat" "%ProgramFiles%\fw-fanctrl" > nul + @echo off + if %errorLevel% neq 0 ( + echo unable to install '.\services\windows\run-service.bat' + pause + exit /b 4 + ) + + echo installing '.\services\windows\run-service.bat' to '%ProgramFiles%\fw-fanctrl' + @echo on + copy /v ".\services\windows\run-service.bat" "%ProgramFiles%\fw-fanctrl" > nul + @echo off + if %errorLevel% neq 0 ( + echo unable to install '.\services\windows\run-service.bat' + pause + exit /b 4 + ) + + set "no-battery-sensor-option=" + + if defined ARG_no-battery-sensor ( + set "no-battery-sensor-option=--no-battery-sensors" + ) + + powershell -Command "(gc '%ProgramFiles%\fw-fanctrl\run-service.bat') -replace '####CONFIG_PATH####', '%Appdata%\fw-fanctrl\config.json' -replace '####NO_BATTERY_SENSOR_OPTION####', '%no-battery-sensor-option%' | Out-File -encoding ASCII '%ProgramFiles%\fw-fanctrl\run-service.bat'" + + echo installing '.\services\windows\fw-fanctrl.bat' to '%ProgramFiles%\fw-fanctrl' + @echo on + copy /v ".\services\windows\fw-fanctrl.bat" "%ProgramFiles%\fw-fanctrl" > nul + @echo off + if %errorLevel% neq 0 ( + echo unable to install '.\services\windows\fw-fanctrl.bat' + pause + exit /b 4 + ) + + powershell -Command "(gc '%ProgramFiles%\fw-fanctrl\fw-fanctrl.bat') -replace '####PYTHON_PATH####', '%localAppData%\Programs\Python\Python312\python' | Out-File -encoding ASCII '%ProgramFiles%\fw-fanctrl\fw-fanctrl.bat'" + + echo creating directory '%Appdata%\fw-fanctrl' + if not exist "%Appdata%\fw-fanctrl" mkdir "%Appdata%\fw-fanctrl" + + echo installing 'config.json' in '%Appdata%\fw-fanctrl\config.json' + if not exist "%Appdata%\fw-fanctrl\config.json" echo n | copy /-y ".\config.json" "%Appdata%\fw-fanctrl\config.json" > nul + + echo creating 'fw-fanctrl' service + @echo on + "%ProgramFiles%\nssm\nssm" install "fw-fanctrl" "%ProgramFiles%\fw-fanctrl\run-service.bat" + "%ProgramFiles%\nssm\nssm" set "fw-fanctrl" Start "SERVICE_DELAYED_AUTO_START" + "%ProgramFiles%\nssm\nssm" set "fw-fanctrl" DisplayName "Framework Fanctrl" + "%ProgramFiles%\nssm\nssm" set "fw-fanctrl" Description "A simple systemd service to better control Framework Laptop's fan(s)" + "%ProgramFiles%\nssm\nssm" set "fw-fanctrl" AppExit "%ProgramFiles%\ectool\ectool autofanctrl" + "%ProgramFiles%\nssm\nssm" set "fw-fanctrl" AppStdout "%ProgramFiles%\fw-fanctrl\out.log" + "%ProgramFiles%\nssm\nssm" set "fw-fanctrl" AppStderr "%ProgramFiles%\fw-fanctrl\out.log" + @echo off + + echo creating service pause/resume on sleep/wake tasks + @echo on + schtasks /create /tn "fw-fanctrl_pauseOnSleep" /tr "powershell.exe -Command 'fw-fanctrl pause'" /sc onevent /ec System /mo "*[System[Provider[@Name='Kernel-Power'] and (EventID=42 or EventID=40)]]" /ru SYSTEM /RL HIGHEST + schtasks /create /tn "fw-fanctrl_resumeOnWake" /tr "powershell.exe -Command 'fw-fanctrl resume'" /sc onevent /ec System /mo "*[System[Provider[@Name='Power-Troubleshooter'] and (EventID=1)]]" /ru SYSTEM /RL HIGHEST + @echo off + + GOTO :EOF +GOTO :EOF + +:UNINSTALL + echo uninstalling + + CALL :uninstall-fw-fanctrl + + CALL :uninstall-nssm + + CALL :uninstall-ectool + + CALL :uninstall-crosec + + rmdir /s /q ".temp" 2> nul + + pause + + GOTO :EOF +GOTO :EOF + +:uninstall-fw-fanctrl + echo removing 'fw-fanctrl' + + echo removing service pause/resume on sleep/wake tasks + @echo on + schtasks /delete /tn "fw-fanctrl_pauseOnSleep" /f + schtasks /delete /tn "fw-fanctrl_resumeOnWake" /f + @echo off + + echo stopping 'fw-fanctrl' service + @echo on + "%ProgramFiles%\nssm\nssm" stop "fw-fanctrl" + @echo off + + echo removing 'fw-fanctrl' service + @echo on + "%ProgramFiles%\nssm\nssm" remove "fw-fanctrl" confirm + @echo off + + echo removing directory '%ProgramFiles%\fw-fanctrl' + rmdir /s /q "%ProgramFiles%\fw-fanctrl" 2> nul + + GOTO :EOF +GOTO :EOF + +:uninstall-nssm + echo removing 'nssm' + + echo removing directory '%ProgramFiles%\nssm' + rmdir /s /q "%ProgramFiles%\nssm" 2> nul + + GOTO :EOF +GOTO :EOF + +:uninstall-ectool + echo removing 'ectool' + + echo setting the fan control back to normal + @echo on + "%ProgramFiles%\ectool\ectool" autofanctrl + @echo off + + echo removing directory '%ProgramFiles%\ectool' + rmdir /s /q "%ProgramFiles%\ectool" 2> nul + + GOTO :EOF +GOTO :EOF + +:uninstall-crosec + echo removing 'crosec' + + echo uninstalling 'crosec' driver + @echo on + "%ProgramFiles%\crosec\installer" uninstall + @echo off + + echo removing directory '%ProgramFiles%\crosec' + rmdir /s /q "%ProgramFiles%\crosec" 2> nul + + echo disabling 'bcdedit testsigning' + bcdedit /set {default} testsigning off + + GOTO :EOF +GOTO :EOF + +:ARG-PARSER + :: Loop until two consecutive empty args + :loopargs + IF "%~1%~2" EQU "" GOTO :EOF + + set "arg1=%~1" + set "arg2=%~2" + shift + + :: Allow either / or - + set "tst1=%arg1:-=/%" + if "%arg1%" NEQ "" ( + set "tst1=%tst1:~0,1%" + ) ELSE ( + set "tst1=" + ) + + set "tst2=%arg2:-=/%" + if "%arg2%" NEQ "" ( + set "tst2=%tst2:~0,1%" + ) ELSE ( + set "tst2=" + ) + + + :: Capture assignments (eg. /foo bar) + IF "%tst1%" EQU "/" IF "%tst2%" NEQ "/" IF "%tst2%" NEQ "" ( + set "ARG_%arg1:~1%=%arg2%" + GOTO loopargs + ) + + :: Capture flags (eg. /foo) + IF "%tst1%" EQU "/" ( + set "ARG_%arg1:~1%=1" + GOTO loopargs + ) + GOTO loopargs + GOTO :EOF + diff --git a/install.sh b/install.sh deleted file mode 100755 index 2ae6cfa..0000000 --- a/install.sh +++ /dev/null @@ -1,221 +0,0 @@ -#!/bin/bash -set -e - -if [ "$EUID" -ne 0 ] - then echo "This program requires root permissions" - exit 1 -fi - -# Argument parsing -SHORT=r,d:,p:,s:,h -LONG=remove,dest-dir:,prefix-dir:,sysconf-dir:,no-ectool,no-pre-uninstall,no-post-install,help -VALID_ARGS=$(getopt -a --options $SHORT --longoptions $LONG -- "$@") -if [[ $? -ne 0 ]]; then - exit 1; -fi - -TEMP_FOLDER='./.temp' -trap 'rm -rf $TEMP_FOLDER' EXIT - -PREFIX_DIR="/usr" -DEST_DIR="" -SYSCONF_DIR="/etc" -SHOULD_INSTALL_ECTOOL=true -SHOULD_PRE_UNINSTALL=true -SHOULD_POST_INSTALL=true -SHOULD_REMOVE=false - -eval set -- "$VALID_ARGS" -while true; do - case "$1" in - '--remove' | '-r') - SHOULD_REMOVE=true - ;; - '--prefix-dir' | '-p') - PREFIX_DIR=$2 - shift - ;; - '--dest-dir' | '-d') - DEST_DIR=$2 - shift - ;; - '--sysconf-dir' | '-s') - SYSCONF_DIR=$2 - shift - ;; - '--no-ectool') - SHOULD_INSTALL_ECTOOL=false - ;; - '--no-pre-uninstall') - SHOULD_PRE_UNINSTALL=false - ;; - '--no-post-install') - SHOULD_POST_INSTALL=false - ;; - '--help' | '-h') - echo "Usage: $0 [--remove,-r] [--dest-dir,-d ] [--prefix-dir,-p ] [--sysconf-dir,-s system configuration destination directory (defaults to $SYSCONF_DIR)] [--no-ectool] [--no-post-install] [--no-pre-uninstall]" 1>&2 - exit 0 - ;; - --) - break - ;; - esac - shift -done -# - -SERVICES_DIR="./services" -SERVICE_EXTENSION=".service" - -SERVICES="$(cd "$SERVICES_DIR" && find . -maxdepth 1 -maxdepth 1 -type f -name "*$SERVICE_EXTENSION" -exec basename {} "$SERVICE_EXTENSION" \;)" -SERVICES_SUBCONFIGS="$(cd "$SERVICES_DIR" && find . -mindepth 1 -maxdepth 1 -type d -exec basename {} \;)" - -function sanitizePath() { - local SANITIZED_PATH="$1" - local SANITIZED_PATH=${SANITIZED_PATH//..\//} - local SANITIZED_PATH=${SANITIZED_PATH#./} - local SANITIZED_PATH=${SANITIZED_PATH#/} - echo "$SANITIZED_PATH" -} - -# remove remaining legacy files -function uninstall_legacy() { - echo "removing legacy files" - rm "/usr/local/bin/fw-fanctrl" 2> "/dev/null" || true - rm "/usr/local/bin/ectool" 2> "/dev/null" || true - rm "/usr/local/bin/fanctrl.py" 2> "/dev/null" || true - rm "/etc/systemd/system/fw-fanctrl.service" 2> "/dev/null" || true -} - -function uninstall() { - if [ "$SHOULD_PRE_UNINSTALL" = true ]; then - ./pre-uninstall.sh - fi - # remove program services based on the services present in the './services' folder - echo "removing services" - for SERVICE in $SERVICES ; do - SERVICE=$(sanitizePath "$SERVICE") - # be EXTRA CAREFUL about the validity of the paths (dont wanna delete something important, right?... O_O) - rm -rf "$DEST_DIR$PREFIX_DIR/lib/systemd/system/$SERVICE$SERVICE_EXTENSION" - done - - # remove program services sub-configurations based on the sub-configurations present in the './services' folder - echo "removing services sub-configurations" - for SERVICE in $SERVICES_SUBCONFIGS ; do - SERVICE=$(sanitizePath "$SERVICE") - echo "removing sub-configurations for [$SERVICE]" - SUBCONFIGS="$(cd "$SERVICES_DIR/$SERVICE" && find . -mindepth 1 -type f)" - for SUBCONFIG in $SUBCONFIGS ; do - SUBCONFIG=$(sanitizePath "$SUBCONFIG") - echo "removing '$DEST_DIR$PREFIX_DIR/lib/systemd/$SERVICE/$SUBCONFIG'" - rm -rf "$DEST_DIR$PREFIX_DIR/lib/systemd/$SERVICE/$SUBCONFIG" 2> "/dev/null" || true - done - done - - rm "$DEST_DIR$PREFIX_DIR/bin/fw-fanctrl" 2> "/dev/null" || true - ectool autofanctrl 2> "/dev/null" || true # restore default fan manager - if [ "$SHOULD_INSTALL_ECTOOL" = true ]; then - rm "$DEST_DIR$PREFIX_DIR/bin/ectool" 2> "/dev/null" || true - fi - rm -rf "$DEST_DIR$SYSCONF_DIR/fw-fanctrl" 2> "/dev/null" || true - rm -rf "/run/fw-fanctrl" 2> "/dev/null" || true - - uninstall_legacy -} - -function install() { - uninstall_legacy - - rm -rf "$TEMP_FOLDER" - mkdir -p "$DEST_DIR$PREFIX_DIR/bin" - if [ "$SHOULD_INSTALL_ECTOOL" = true ]; then - mkdir "$TEMP_FOLDER" - installEctool "$TEMP_FOLDER" || (echo "an error occurred when installing ectool." && echo "please check your internet connection or consider installing it manually and using --no-ectool on the installation script." && exit 1) - rm -rf "$TEMP_FOLDER" - fi - mkdir -p "$DEST_DIR$SYSCONF_DIR/fw-fanctrl" - cp "./fanctrl.py" "$DEST_DIR$PREFIX_DIR/bin/fw-fanctrl" - chmod +x "$DEST_DIR$PREFIX_DIR/bin/fw-fanctrl" - - cp -n "./config.json" "$DEST_DIR$SYSCONF_DIR/fw-fanctrl" 2> "/dev/null" || true - - # create program services based on the services present in the './services' folder - echo "creating '$DEST_DIR$PREFIX_DIR/lib/systemd/system'" - mkdir -p "$DEST_DIR$PREFIX_DIR/lib/systemd/system" - echo "creating services" - for SERVICE in $SERVICES ; do - SERVICE=$(sanitizePath "$SERVICE") - if [ "$SHOULD_PRE_UNINSTALL" = true ] && [ "$(systemctl is-active "$SERVICE")" == "active" ]; then - echo "stopping [$SERVICE]" - systemctl stop "$SERVICE" - fi - echo "creating '$DEST_DIR$PREFIX_DIR/lib/systemd/system/$SERVICE$SERVICE_EXTENSION'" - cat "$SERVICES_DIR/$SERVICE$SERVICE_EXTENSION" | sed -e "s/%PREFIX_DIRECTORY%/${PREFIX_DIR//\//\\/}/" | sed -e "s/%SYSCONF_DIRECTORY%/${SYSCONF_DIR//\//\\/}/" | tee "$DEST_DIR$PREFIX_DIR/lib/systemd/system/$SERVICE$SERVICE_EXTENSION" > "/dev/null" - done - - # add program services sub-configurations based on the sub-configurations present in the './services' folder - echo "adding services sub-configurations" - for SERVICE in $SERVICES_SUBCONFIGS ; do - SERVICE=$(sanitizePath "$SERVICE") - echo "adding sub-configurations for [$SERVICE]" - SUBCONFIG_FOLDERS="$(cd "$SERVICES_DIR/$SERVICE" && find . -mindepth 1 -maxdepth 1 -type d -exec basename {} \;)" - # ensure folders exists - mkdir -p "$DEST_DIR$PREFIX_DIR/lib/systemd/$SERVICE" - for SUBCONFIG_FOLDER in $SUBCONFIG_FOLDERS ; do - SUBCONFIG_FOLDER=$(sanitizePath "$SUBCONFIG_FOLDER") - echo "creating '$DEST_DIR$PREFIX_DIR/lib/systemd/$SERVICE/$SUBCONFIG_FOLDER'" - mkdir -p "$DEST_DIR$PREFIX_DIR/lib/systemd/$SERVICE/$SUBCONFIG_FOLDER" - done - SUBCONFIGS="$(cd "$SERVICES_DIR/$SERVICE" && find . -mindepth 1 -type f)" - # add sub-configurations - for SUBCONFIG in $SUBCONFIGS ; do - SUBCONFIG=$(sanitizePath "$SUBCONFIG") - echo "adding '$DEST_DIR$PREFIX_DIR/lib/systemd/$SERVICE/$SUBCONFIG'" - cat "$SERVICES_DIR/$SERVICE/$SUBCONFIG" | sed -e "s/%PREFIX_DIRECTORY%/${PREFIX_DIR//\//\\/}/" | tee "$DEST_DIR$PREFIX_DIR/lib/systemd/$SERVICE/$SUBCONFIG" > "/dev/null" - chmod +x "$DEST_DIR$PREFIX_DIR/lib/systemd/$SERVICE/$SUBCONFIG" - done - done - if [ "$SHOULD_POST_INSTALL" = true ]; then - ./post-install.sh --dest-dir "$DEST_DIR" --sysconf-dir "$SYSCONF_DIR" - fi -} - -function installEctool() { - workingDirectory=$1 - echo "installing ectool" - - ectoolDestPath="$DEST_DIR$PREFIX_DIR/bin/ectool" - - ectoolJobId="$(cat './fetch/ectool/linux/gitlab_job_id')" - ectoolSha256Hash="$(cat './fetch/ectool/linux/hash.sha256')" - - artifactsZipFile="$workingDirectory/artifact.zip" - - echo "downloading artifact from gitlab" - curl -s -S -o "$artifactsZipFile" -L "https://gitlab.howett.net/DHowett/ectool/-/jobs/${ectoolJobId}/artifacts/download?file_type=archive" || (echo "failed to download the artifact." && return 1) - if [[ $? -ne 0 ]]; then return 1; fi - - echo "checking artifact sha256 sum" - actualEctoolSha256Hash=$(sha256sum "$artifactsZipFile" | cut -d ' ' -f 1) - if [[ "$actualEctoolSha256Hash" != "$ectoolSha256Hash" ]]; then - echo "Incorrect sha256 sum for ectool gitlab artifact '$ectoolJobId' : '$ectoolSha256Hash' != '$actualEctoolSha256Hash'" - return 1 - fi - - echo "extracting artifact" - { - unzip -q -j "$artifactsZipFile" '_build/src/ectool' -d "$workingDirectory" && - cp "$workingDirectory/ectool" "$ectoolDestPath" && - chmod +x "$ectoolDestPath" - } || (echo "failed to extract the artifact to its designated location." && return 1) - if [[ $? -ne 0 ]]; then return 1; fi - - echo "ectool installed" -} - -if [ "$SHOULD_REMOVE" = true ]; then - uninstall -else - install -fi -exit 0 diff --git a/post-install.sh b/post-install.sh deleted file mode 100755 index c92e74a..0000000 --- a/post-install.sh +++ /dev/null @@ -1,74 +0,0 @@ -#!/bin/bash -set -e - -if [ "$EUID" -ne 0 ] - then echo "This program requires root permissions" - exit 1 -fi - -HOME_DIR="$(eval echo "~$(logname)")" - -# Argument parsing -SHORT=d:,s:,h -LONG=dest-dir:,sysconf-dir:,help -VALID_ARGS=$(getopt -a --options $SHORT --longoptions $LONG -- "$@") -if [[ $? -ne 0 ]]; then - exit 1; -fi - -DEST_DIR="/usr" -SYSCONF_DIR="/etc" - -eval set -- "$VALID_ARGS" -while true; do - case "$1" in - '--dest-dir' | '-d') - DEST_DIR=$2 - shift - ;; - '--sysconf-dir' | '-s') - SYSCONF_DIR=$2 - shift - ;; - '--help' | '-h') - echo "Usage: $0 [--dest-dir,-d ] [--sysconf-dir,-s system configuration destination directory (defaults to $SYSCONF_DIR)]" 1>&2 - exit 0 - ;; - --) - break - ;; - esac - shift -done -# - -SERVICES_DIR="./services" -SERVICE_EXTENSION=".service" - -SERVICES="$(cd "$SERVICES_DIR" && find . -maxdepth 1 -maxdepth 1 -type f -name "*$SERVICE_EXTENSION" -exec basename {} "$SERVICE_EXTENSION" \;)" - -function sanitizePath() { - local SANITIZED_PATH="$1" - local SANITIZED_PATH=${SANITIZED_PATH//..\//} - local SANITIZED_PATH=${SANITIZED_PATH#./} - local SANITIZED_PATH=${SANITIZED_PATH#/} - echo "$SANITIZED_PATH" -} - -# move remaining legacy files -function move_legacy() { - echo "moving legacy files to their new destination" - (cp "$HOME_DIR/.config/fw-fanctrl"/* "$DEST_DIR$SYSCONF_DIR/fw-fanctrl/" && rm -rf "$HOME_DIR/.config/fw-fanctrl") 2> "/dev/null" || true -} - -move_legacy - -echo "enabling services" -systemctl daemon-reload -for SERVICE in $SERVICES ; do - SERVICE=$(sanitizePath "$SERVICE") - echo "enabling [$SERVICE]" - systemctl enable "$SERVICE" - echo "starting [$SERVICE]" - systemctl start "$SERVICE" -done diff --git a/pre-uninstall.sh b/pre-uninstall.sh deleted file mode 100755 index 9ca3953..0000000 --- a/pre-uninstall.sh +++ /dev/null @@ -1,55 +0,0 @@ -#!/bin/bash -set -e - -if [ "$EUID" -ne 0 ] - then echo "This program requires root permissions" - exit 1 -fi - -HOME_DIR="$(eval echo "~$(logname)")" - -# Argument parsing -SHORT=h -LONG=help -VALID_ARGS=$(getopt -a --options $SHORT --longoptions $LONG -- "$@") -if [[ $? -ne 0 ]]; then - exit 1; -fi - -eval set -- "$VALID_ARGS" -while true; do - case "$1" in - '--help' | '-h') - echo "Usage: $0" 1>&2 - exit 0 - ;; - --) - break - ;; - esac - shift -done -# - -SERVICES_DIR="./services" -SERVICE_EXTENSION=".service" - -SERVICES="$(cd "$SERVICES_DIR" && find . -maxdepth 1 -maxdepth 1 -type f -name "*$SERVICE_EXTENSION" -exec basename {} "$SERVICE_EXTENSION" \;)" - -function sanitizePath() { - local SANITIZED_PATH="$1" - local SANITIZED_PATH=${SANITIZED_PATH//..\//} - local SANITIZED_PATH=${SANITIZED_PATH#./} - local SANITIZED_PATH=${SANITIZED_PATH#/} - echo "$SANITIZED_PATH" -} - -echo "disabling services" -systemctl daemon-reload -for SERVICE in $SERVICES ; do - SERVICE=$(sanitizePath "$SERVICE") - echo "stopping [$SERVICE]" - systemctl stop "$SERVICE" 2> "/dev/null" || true - echo "disabling [$SERVICE]" - systemctl disable "$SERVICE" 2> "/dev/null" || true -done diff --git a/services/fw-fanctrl.service b/services/fw-fanctrl.service deleted file mode 100644 index 7f70d58..0000000 --- a/services/fw-fanctrl.service +++ /dev/null @@ -1,10 +0,0 @@ -[Unit] -Description=Framework Fan Controller -After=multi-user.target -[Service] -Type=simple -Restart=always -ExecStart=/usr/bin/python3 "%PREFIX_DIRECTORY%/bin/fw-fanctrl" --run --config "%SYSCONF_DIRECTORY%/fw-fanctrl/config.json" --no-log -ExecStopPost=/bin/sh -c "ectool autofanctrl" -[Install] -WantedBy=multi-user.target diff --git a/services/system-sleep/fw-fanctrl-suspend b/services/system-sleep/fw-fanctrl-suspend deleted file mode 100644 index 1fb3952..0000000 --- a/services/system-sleep/fw-fanctrl-suspend +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh - -case $1 in - pre) /usr/bin/python3 "%PREFIX_DIRECTORY%/bin/fw-fanctrl" --pause ;; - post) /usr/bin/python3 "%PREFIX_DIRECTORY%/bin/fw-fanctrl" --resume ;; -esac diff --git a/services/windows/fw-fanctrl.bat b/services/windows/fw-fanctrl.bat new file mode 100644 index 0000000..57c4910 --- /dev/null +++ b/services/windows/fw-fanctrl.bat @@ -0,0 +1,3 @@ +@setlocal + +####PYTHON_PATH#### "%ProgramFiles%\fw-fanctrl\fanctrl.py" --socket-controller win32 %* diff --git a/services/windows/run-service.bat b/services/windows/run-service.bat new file mode 100644 index 0000000..c408ba4 --- /dev/null +++ b/services/windows/run-service.bat @@ -0,0 +1,8 @@ +@setlocal + +@cd /d "%~dp0" + +fw-fanctrl run --config "####CONFIG_PATH####" --silent ####NO_BATTERY_SENSOR_OPTION#### & ectool autofanctrl + +@echo "waiting 5 seconds before retrying..." +@timeout 5 > NUL diff --git a/uninstall.bat b/uninstall.bat new file mode 100644 index 0000000..246463a --- /dev/null +++ b/uninstall.bat @@ -0,0 +1,3 @@ +cd /d "%~dp0" + +.\install.bat /r \ No newline at end of file