Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions emhttp/languages/en_US/helptext.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1648,6 +1648,18 @@ Choose if rebooting or powering down the server needs a confirmation checkbox.
Choose if stopping the array needs a confirmation checkbox.
:end

:powerbutton_behavior_help:
Choose what happens when the server's physical power button is pressed.

**Shut down** performs a clean shutdown (stops the array and powers off). This is the default behavior.

**Reboot** performs a clean reboot.

**Do nothing** ignores the button press and only logs the event. This is useful on small form factor hardware where the power button is easily bumped, to prevent accidental shutdowns.

Plugins may add further actions (for example, Sleep via the S3 Sleep plugin, or running a User Script).
:end

:display_settings_help:
The display settings below determine how items are displayed on screen. Use these settings to tweak the visual effects to your likings.

Expand Down
60 changes: 60 additions & 0 deletions emhttp/plugins/dynamix/PowerButton.page
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
Menu="PowerMode:2"
Title="Power Button"
Tag="icon-power"
---
<?PHP
/* Copyright 2005-2025, Lime Technology
* Copyright 2012-2025, Bergware International.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*/
?>
<?
$cur = _var($powermode,'powerbutton','shutdown');
?>

<form markdown="1" name="PowerButton" method="POST" action="/update.php" target="progressFrame">
<input type="hidden" name="#file" value="dynamix/dynamix.cfg">
<input type="hidden" name="#section" value="powermode">

_(Physical power button)_:
: <select name="powerbutton" id="powerbutton" onchange="powerbuttonExtra(this)">
<?=mk_option($cur, "shutdown", _('Shut down').' ('._('default').')')?>
<?=mk_option($cur, "reboot", _('Reboot'))?>
<?=mk_option($cur, "ignore", _('Do nothing'))?>
<?php
// Extension point: plugins add <option> lines for custom actions.
// In scope: $cur (current powerbutton value). See /etc/acpi/powerbutton.d/README.
foreach (glob('/usr/local/emhttp/plugins/*/include/powerbutton-option.php') ?: [] as $frag) include $frag;
?>
</select>

:powerbutton_behavior_help:

<?php
// Extension point: plugins add config rows shown when their action is selected.
// Each fragment outputs: <div class="powerbutton-extra" data-action="<action>" markdown="1"> ...fields... </div>
foreach (glob('/usr/local/emhttp/plugins/*/include/powerbutton-extra.php') ?: [] as $frag) include $frag;
?>

&nbsp;
: <span class="inline-block">
<input type="submit" name="#apply" value="_(Apply)_" disabled>
<input type="button" value="_(Done)_" onclick="done()">
</span>
</form>

<script>
function powerbuttonExtra(sel) {
var v = sel.value;
$('.powerbutton-extra').each(function(){
$(this).toggle($(this).data('action') === v);
});
}
$(function(){ powerbuttonExtra(document.getElementById('powerbutton')); });
</script>
75 changes: 1 addition & 74 deletions emhttp/plugins/dynamix/PowerMode.page
Original file line number Diff line number Diff line change
@@ -1,77 +1,4 @@
Menu="OtherSettings"
Type="xmenu"
Title="Power Mode"
Title="Power Options"
Icon="icon-energysaving"
Tag="icon-energysaving"
---
<?PHP
/* Copyright 2005-2023, Lime Technology
* Copyright 2012-2023, Bergware International.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*/
?>
<?
$cpufreq = '/sys/devices/system/cpu/cpu0/cpufreq';
$current = exec("cat $cpufreq/scaling_governor 2>/dev/null");
exec("cat $cpufreq/scaling_available_governors 2>/dev/null | tr ' ' '\n' | sed '/^$/d' | sort -u",$governor);

function value(...$modes) {
global $current, $governor;
$checked = $value = '';
$disabled = ' disabled';
foreach ($modes as $mode) {
if ($mode==$current) $checked = ' checked';
if (in_array($mode,$governor)) {$value = "value=\"$mode\""; $disabled = '';}
}
return $value.$checked.$disabled;
}
?>
<script>
function preparePowermode(form) {
$(form).find('[name="#arg[1]"]').val(form.powermode.value);
}
$(function(){
$('input[type=radio]').each(function(){
if ($(this).prop('disabled')) $(this).next('span').html(" <i>(_(unavailable)_)</i>");
});
<?if (exec("dmesg | grep -Pom1 'Hypervisor detected'")):?>
$('#vm').show();
<?endif;?>
});
</script>

<form markdown="1" name="PowerMode" method="POST" action="/update.php" target="progressFrame" onsubmit="preparePowermode(this)">
<input type="hidden" name="#file" value="dynamix/dynamix.cfg">
<input type="hidden" name="#section" value="powermode">
<input type="hidden" name="#command" value="/webGui/scripts/powermode">
<input type="hidden" name="#arg[1]" value="">

_(Change power mode)_:
: <span>
<input name="powermode" type="radio"<?=value('powersave')?>>_(Best power efficiency)_
</span>

&nbsp;
: <span>
<input name="powermode" type="radio"<?=value('ondemand','balance_power')?>>_(Balanced operation)_
</span>

&nbsp;
: <span>
<input name="powermode" type="radio"<?=value('performance')?>>_(Best performance)_
</span>

&nbsp;
: <span class="inline-block">
<input type="submit" name="#apply" value="_(Apply)_" disabled>
<input type="button" value="_(Done)_" onclick="done()">
</span>
</form>

<div id="vm" class="notice">_(When running Unraid virtualized, there are no available power modes)_</div>
75 changes: 75 additions & 0 deletions emhttp/plugins/dynamix/PowerModeCpu.page
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
Menu="PowerMode:1"
Title="Power Mode"
Tag="icon-energysaving"
---
<?PHP
/* Copyright 2005-2023, Lime Technology
* Copyright 2012-2023, Bergware International.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*/
?>
<?
$cpufreq = '/sys/devices/system/cpu/cpu0/cpufreq';
$current = exec("cat $cpufreq/scaling_governor 2>/dev/null");
exec("cat $cpufreq/scaling_available_governors 2>/dev/null | tr ' ' '\n' | sed '/^$/d' | sort -u",$governor);

function value(...$modes) {
global $current, $governor;
$checked = $value = '';
$disabled = ' disabled';
foreach ($modes as $mode) {
if ($mode==$current) $checked = ' checked';
if (in_array($mode,$governor)) {$value = "value=\"$mode\""; $disabled = '';}
}
return $value.$checked.$disabled;
}
?>
<script>
function preparePowermode(form) {
$(form).find('[name="#arg[1]"]').val(form.powermode.value);
}
$(function(){
$('input[type=radio]').each(function(){
if ($(this).prop('disabled')) $(this).next('span').html(" <i>(_(unavailable)_)</i>");
});
Comment on lines +38 to +40

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Unavailable-label injection currently never matches the DOM.

At Line 39, $(this).next('span') does not find anything because the radio inputs are followed by text nodes, not <span> siblings. As written, disabled options won’t get the “unavailable” note.

Suggested fix
 $(function(){
   $('input[type=radio]').each(function(){
-    if ($(this).prop('disabled')) $(this).next('span').html(" <i>(_(unavailable)_)</i>");
+    if ($(this).prop('disabled')) {
+      $(this).after(" <i>(_(unavailable)_)</i>");
+    }
   });
 <?if (exec("dmesg | grep -Pom1 'Hypervisor detected'")):?>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$('input[type=radio]').each(function(){
if ($(this).prop('disabled')) $(this).next('span').html(" <i>(_(unavailable)_)</i>");
});
$(function(){
$('input[type=radio]').each(function(){
if ($(this).prop('disabled')) {
$(this).after(" <i>(_(unavailable)_)</i>");
}
});
<?if (exec("dmesg | grep -Pom1 'Hypervisor detected'")):?>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@emhttp/plugins/dynamix/PowerModeCpu.page` around lines 38 - 40, The jQuery
selector `$(this).next('span')` in the disabled radio button check loop is not
finding the correct elements because radio inputs are not directly followed by
span siblings, but rather by text nodes or differently structured elements.
Inspect the actual HTML structure around the radio inputs to determine what
element actually follows them, then update the traversal method in the loop
(replacing `.next('span')`) to correctly locate the element that should receive
the unavailable label text. This may require using a different jQuery method
like .closest(), .parent(), or .find() depending on the actual DOM hierarchy.

<?if (exec("dmesg | grep -Pom1 'Hypervisor detected'")):?>
$('#vm').show();
<?endif;?>
});
</script>

<form markdown="1" name="PowerMode" method="POST" action="/update.php" target="progressFrame" onsubmit="preparePowermode(this)">
<input type="hidden" name="#file" value="dynamix/dynamix.cfg">
<input type="hidden" name="#section" value="powermode">
<input type="hidden" name="#command" value="/webGui/scripts/powermode">
<input type="hidden" name="#arg[1]" value="">

_(Change power mode)_:
: <span>
<input name="powermode" type="radio"<?=value('powersave')?>>_(Best power efficiency)_
</span>

&nbsp;
: <span>
<input name="powermode" type="radio"<?=value('ondemand','balance_power')?>>_(Balanced operation)_
</span>

&nbsp;
: <span>
<input name="powermode" type="radio"<?=value('performance')?>>_(Best performance)_
</span>

&nbsp;
: <span class="inline-block">
<input type="submit" name="#apply" value="_(Apply)_" disabled>
<input type="button" value="_(Done)_" onclick="done()">
</span>
</form>

<div id="vm" class="notice">_(When running Unraid virtualized, there are no available power modes)_</div>
48 changes: 48 additions & 0 deletions etc/acpi/powerbutton.d/README
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/etc/acpi/powerbutton.d/ - Unraid power button action handlers (extension point)
================================================================================

The physical power button behavior is configured in the WebUI at
Settings > Power Options > Power Button. The selected action is stored as
[powermode]
powerbutton="<action>"
in /boot/config/plugins/dynamix/dynamix.cfg and dispatched at press time by
/etc/acpi/unraid_power_handler.sh.

Built-in actions: shutdown (default), reboot, ignore.

Actions that need their own setup or prep (suspend/sleep, running a User
Script, etc.) are NOT built in - they are provided by plugins through the
two-part hook below, so core stays free of plugin-specific or hardware-risky
behavior.

Plugins can ADD or OVERRIDE actions in two parts:

1) Action handler (required)
Ship an executable here, named after the action value:
/etc/acpi/powerbutton.d/<action>
It runs when the power button is pressed and that action is selected.
(e.g. the S3 Sleep plugin ships powerbutton.d/sleep to suspend with its
own array-aware prep.)

2) WebUI option (so users can pick it)
Add the action to the selector by shipping:
/usr/local/emhttp/plugins/<plugin>/include/powerbutton-option.php
which echoes one or more options, e.g.:
<?=mk_option($cur, "script", _('Run script'))?>
($cur is the currently saved powerbutton value.)

For extra fields shown only when your action is selected, also ship:
/usr/local/emhttp/plugins/<plugin>/include/powerbutton-extra.php
which outputs a block tagged with your action value:
<div class="powerbutton-extra" data-action="script" markdown="1">
_(Script)_:
: <select name="powerbuttonscript"> ... </select>
</div>
Fields are POSTed into dynamix.cfg [powermode] alongside powerbutton, so
your handler can read them with the same grep pattern used above.

Example (User Scripts plugin, "Run script" action):
- include/powerbutton-option.php : adds the "script" option + script picker
- include/powerbutton-extra.php : script selection dropdown
- /etc/acpi/powerbutton.d/script : reads powerbuttonscript= from dynamix.cfg
and executes the chosen user script
68 changes: 68 additions & 0 deletions etc/acpi/unraid_power_handler.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#!/bin/bash
#
# script: unraid_power_handler.sh
#
# Unraid ACPI event handler. Installed over /etc/acpi/acpi_handler.sh at boot
# by /etc/rc.d/rc.acpid, so it reliably overrides the stock Slackware acpid
# handler regardless of package install order.
#
# The physical power button behavior is configurable from the WebUI
# (Settings > Power Options > Power Button). The choice is stored in
# dynamix.cfg and read live on each event, so no acpid reload is needed
# when the setting changes.
#
# Plugins can add or override actions by dropping an executable handler in
# /etc/acpi/powerbutton.d/<action> - see /etc/acpi/powerbutton.d/README.
#
# LimeTech - modified for Unraid OS

CFG=/boot/config/plugins/dynamix/dynamix.cfg
PLUGIN_DIR=/etc/acpi/powerbutton.d

# acpid passes the raw event line, e.g.: "button/power PBTN 00000080 00000000"
set -- $@
group=${1%%/*}
action=${1#*/}

run_powerbutton(){
local behavior=$1
case "$behavior" in
ignore)
logger -t acpid "Power button: ignored (Power Options setting)"
;;
reboot)
logger -t acpid "Power button: rebooting"
/sbin/init 6
;;
shutdown)
logger -t acpid "Power button: initiating clean shutdown"
/sbin/init 0
;;
*)
# Extension point: plugin-provided action handler.
if [[ -n $behavior && -x $PLUGIN_DIR/$behavior ]]; then
logger -t acpid "Power button: running plugin action '$behavior'"
"$PLUGIN_DIR/$behavior"
else
logger -t acpid "Power button: unknown action '$behavior', ignoring"
fi
;;
esac
}

case "$group" in
button)
case "$action" in
power)
behavior=$(grep -Pom1 '^powerbutton="\K[^"]+' "$CFG" 2>/dev/null)
run_powerbutton "${behavior:-shutdown}"
;;
*)
logger -t acpid "ACPI action button/$action is not handled"
;;
esac
;;
*)
logger -t acpid "ACPI event $1 is not handled"
;;
esac
9 changes: 9 additions & 0 deletions etc/elogind/logind.conf.d/20-unraid-powerbutton.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Unraid: let acpid be the sole handler of the physical power button.
#
# A power press generates both an ACPI event (handled by acpid via
# /etc/acpi/acpi_handler.sh) and an input key event (handled by elogind).
# To make the configurable power button behavior in Settings > Power Options
# work, elogind must not act on the key, leaving acpid in full control.
[Login]
HandlePowerKey=ignore
HandlePowerKeyLongPress=ignore
15 changes: 15 additions & 0 deletions etc/rc.d/rc.acpid
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,28 @@ acpid_running(){
ps axc | grep -q ' acpid'
}

# Install Unraid's ACPI event handler over the stock Slackware acpi_handler.sh.
# Done at boot so it reliably wins regardless of package install order. The
# handler reads the configurable power button behavior (Settings > Power
# Options > Power Button) live from dynamix.cfg, so no reload is needed when
# the setting changes.
acpid_configure(){
local SRC=/etc/acpi/unraid_power_handler.sh
# Drop-in dir for plugin-provided power button actions (see README there).
mkdir -p /etc/acpi/powerbutton.d
if [[ -f $SRC ]]; then
install -m 0755 "$SRC" /etc/acpi/acpi_handler.sh
fi
}

acpid_start(){
log "Starting $DAEMON..."
local REPLY
if acpid_running; then
REPLY="Already started"
else
if [[ -d /proc/acpi ]]; then
acpid_configure
run /usr/sbin/acpid
if acpid_running; then REPLY="Started"; else REPLY="Failed"; fi
else
Expand Down
Loading