Merge remote-tracking branch 'upstream/master' into SR-IOV

This commit is contained in:
SimonFair
2025-12-06 20:59:49 +00:00
28 changed files with 618 additions and 283 deletions

View File

@@ -58,6 +58,7 @@
#guiSearchBoxSpan {
margin: 0;
padding: 0;
height: 18px !important;
}
#guiSearchBox {
position: relative;

View File

@@ -67,6 +67,8 @@ case 'language':
// nothing defined
break;
}
// unset GUI search cache
@unlink("/tmp/gui.search/searchResults.json");
// unset pending status
if ($method != 'check') @unlink("$pending/$name");

View File

@@ -23,9 +23,6 @@ Usage: plugin install PLUGIN-FILE [forced]
forced is optional and can be used to install a lower version than currently running.
This command will process all FILE elements in PLUGIN-FILE which are tagged with the "install" method (or
that have no method tag).
This command has two major use cases:
1) Invoked at system startup by /etc/rc.d/rc.local on each .plg file found int /boot/config/plugins.
@@ -79,6 +76,17 @@ Usage: plugin update PLUGIN
Note: to support `plugin check` and `plugin update` the plugin file must contain both "pluginURL" and
"version" attributes.
Usage: plugin download PLUGIN-FILE [TARGET-VERSION] [forced]
Downloads newer versions of installed plugin files without executing any Run commands.
This method downloads the updated plugin definition file (.plg) to /boot/config/plugins-nextboot/
then downloads all required files to the same directory but skips script execution.
On the next boot, rc.local will move the files from /boot/config/plugins-nextboot/ to /boot/config/plugins/
and install them normally.
This method is suitable for updating plugin files before an Unraid OS upgrade.
TARGET-VERSION is optional and specifies the Unraid version to use for version compatibility
checks (Min/Max attributes). If omitted, the current Unraid version is used.
Usage: plugin [attribute name] PLUGIN-FILE
Any method which is not one of the actions listed above is assumed to be the name of an attribute of
@@ -127,6 +135,12 @@ Here is the set of directories and files used by the plugin system:
successful `plugin install`, the plugin file is copied here (if not here already). Upon successful
`plugin remove`, the plugin file is deleted from here.
/boot/config/plugins-nextboot/
This directory contains the plugin files for plugins to be updated at next boot-time.
Upon successful `plugin download`, plugin files will be located here.
On the next boot, rc.local will move the files from /boot/config/plugins-nextboot/ to /boot/config/plugins/
and install them normally.
/boot/config/plugins-error/
This directory contains plugin files that failed to install.
@@ -202,7 +216,8 @@ function run($command) {
// hook programs receives three parameters: type=plugin and method and plugin-name
//
function pre_hooks() {
global $method, $plugin;
global $method, $plugin, $download_only;
if ($download_only) return;
$hooks = "/usr/local/emhttp/plugins/dynamix.plugin.manager/pre-hooks";
foreach (glob("$hooks/*") as $hook) if (is_executable($hook)) {
write("Executing hook script: ".basename($hook)."\n");
@@ -215,7 +230,8 @@ function pre_hooks() {
// hook programs receives four parameters: type=plugin and method and plugin-name and error (empty if none)
//
function post_hooks($error='') {
global $method, $plugin;
global $method, $plugin, $download_only;
if ($download_only) return;
$hooks = "/usr/local/emhttp/plugins/dynamix.plugin.manager/post-hooks";
foreach (glob("$hooks/*") as $hook) if (is_executable($hook)) {
write("Executing hook script: ".basename($hook)."\n");
@@ -285,7 +301,7 @@ function filter_url($url) {
// is processed for any of those methods.
//
function plugin($method, $plugin_file, &$error) {
global $unraid, $logger;
global $logger, $download_only, $check_version, $boot, $nextboot;
$methods = ['install', 'remove'];
// parse plugin definition XML file
@@ -352,17 +368,31 @@ function plugin($method, $plugin_file, &$error) {
$name = $file->attributes()->Name ?: '';
// bergware - check Unraid version dependency (if present)
$min = $file->attributes()->Min;
if ($min && version_compare($unraid['version'],$min,'<')) {
write("plugin: skipping: ".basename($name)." - Unraid version too low, requires at least version $min\n");
if ($min && version_compare($check_version,$min,'<')) {
if (!$download_only) {
write("plugin: skipping: ".basename($name)." - Unraid version too low, requires at least version $min\n");
}
continue;
}
$max = $file->attributes()->Max;
if ($max && version_compare($unraid['version'],$max,'>')) {
write("plugin: skipping: ".basename($name)." - Unraid version too high, requires at most version $max\n");
if ($max && version_compare($check_version,$max,'>')) {
if (!$download_only) {
write("plugin: skipping: ".basename($name)." - Unraid version too high, requires at most version $max\n");
}
continue;
}
// Name can be missing but only makes sense if Run attribute is present
if ($name) {
// If download_only mode, only process files that go to $boot/ (persistent location)
if ($download_only) {
if (strpos($name, "$boot/") === 0) {
// Redirect $boot/ paths to $nextboot/ to be installed at next boot
$name = "$nextboot/" . substr($name, strlen("$boot/"));
} else {
// Skip other files - they won't persist after boot
continue;
}
}
// Ensure parent directory exists
//
if (!file_exists(dirname($name))) {
@@ -455,6 +485,11 @@ function plugin($method, $plugin_file, &$error) {
//
if ($file->attributes()->Run) {
$command = $file->attributes()->Run;
if ($download_only) {
$target = $name ?: ($file->LOCAL ?: 'inline script');
my_logger("skipping run: $command $target - download-only mode", $logger);
continue;
}
if ($name) {
my_logger("running: $command $name", $logger);
$retval = run("$command $name");
@@ -462,7 +497,6 @@ function plugin($method, $plugin_file, &$error) {
my_logger("running: $command $file->LOCAL", $logger);
$retval = run("$command $file->LOCAL");
} elseif ($file->INLINE) {
$name = '/tmp/inline'.$current_file.'-'.pathinfo($plugin_file, PATHINFO_FILENAME).'.sh';
file_put_contents($name, $file->INLINE);
$exec = $command." ".escapeshellarg($name);
@@ -484,13 +518,44 @@ function move($src_file, $tar_dir) {
return rename($src_file, $tar_dir."/".basename($src_file));
}
$notify = '/usr/local/emhttp/webGui/scripts/notify';
$boot = '/boot/config/plugins';
$plugins = '/var/log/plugins';
$tmp = '/tmp/plugins';
$method = $argv[1];
$builtin = ['unRAIDServer','unRAIDServer-'];
$nchan = $argv[$argc-1] == 'nchan'; // console or nchan output
$notify = '/usr/local/emhttp/webGui/scripts/notify';
$boot = '/boot/config/plugins';
$nextboot = '/boot/config/plugins-nextboot';
$plugins = '/var/log/plugins';
$tmp = '/tmp/plugins';
$download_only = false;
$script = $argv[0] ?? '';
$method = $argv[1] ?? '';
$args = array_slice($argv, 2);
$extra_args = array_slice($args, 1); // anything after the plugin path
$builtin = ['unRAIDServer','unRAIDServer-'];
// Load Unraid version and initialize check_version
$unraid = parse_ini_file('/etc/unraid-version');
$check_version = $unraid['version'];
// Optional flags
// nchan must be the final argument
$nchan = ($argc > 0) && ($argv[$argc-1] === 'nchan'); // console or nchan output
if ($nchan) array_pop($extra_args);
// Extract target version if present (for download/update methods)
// Version pattern: starts with digit, contains dots, optionally has dash suffix (e.g., "7.2.0", "7.2.0-rc.1")
if (!empty($extra_args) && ($method === 'download' || $method === 'update')) {
$first_arg = $extra_args[0];
if (preg_match('/^\d+\.\d+\.\d+(-.*)?$/', $first_arg)) {
$check_version = $first_arg;
array_shift($extra_args);
}
}
$forced = !empty($extra_args); // any extra arg (besides nchan and TARGET-VERSION) signals forced
// Normalize download to reuse the update flow while skipping run steps.
if ($method === 'download') {
$download_only = true;
$method = 'update';
}
// In following code,
// $plugin - is a basename of a plugin, eg, "myplugin.plg"
@@ -506,8 +571,8 @@ if ($argc < 2) {
// check all installed plugins, except built-in
//
if ($method == 'checkall') {
if (!$cmd = realpath($argv[0])) {
write("Unknown command: {$argv[0]}\n");
if (!$cmd = realpath($script)) {
write("Unknown command: {$script}\n");
done(1);
}
foreach (glob("$plugins/*.plg", GLOB_NOSORT) as $link) {
@@ -529,8 +594,8 @@ if ($method == 'checkall') {
// update all installed plugins, which have a update available
//
if ($method == 'updateall') {
if (!$cmd = realpath($argv[0])) {
write("Unknown command: {$argv[0]}\n");
if (!$cmd = realpath($script)) {
write("Unknown command: {$script}\n");
done(1);
}
foreach (glob("$plugins/*.plg", GLOB_NOSORT) as $link) {
@@ -557,8 +622,8 @@ if ($method == 'updateall') {
// check built-in only
//
if ($method == 'checkos') {
if (!$cmd = realpath($argv[0])) {
write("Unknown command: {$argv[0]}\n");
if (!$cmd = realpath($script)) {
write("Unknown command: {$script}\n");
done(1);
}
foreach ($builtin as $link) {
@@ -580,13 +645,12 @@ if ($argc < 3) {
done(1);
}
// plugin install [plugin_file]
// plugin install [plugin_file] / plugin download [plugin_file]
// cases:
// a) dirname of [plugin_file] is /boot/config/plugins (system startup)
// b) [plugin_file] is a URL
// c) dirname of [plugin_file] is not /boot/config/plugins
//
$unraid = parse_ini_file('/etc/unraid-version');
if ($method == 'install') {
$argv[2] = preg_replace('#[\x00-\x1F\x80-\xFF]#', '', $argv[2]);
$plugin = basename($argv[2]);
@@ -594,7 +658,8 @@ if ($method == 'install') {
write("plugin: $plugin is not a plg file\n");
done(1);
}
write("plugin: installing: $plugin\n");
$action = 'installing';
write("plugin: $action: $plugin\n");
// check for URL
if (preg_match('#^https?://#',$argv[2])) {
$pluginURL = $argv[2];
@@ -613,8 +678,9 @@ if ($method == 'install') {
$plugin_file = realpath($argv[2]);
}
// bergware - check Unraid version dependency (if present)
global $check_version;
$min = plugin('min', $plugin_file, $error);
if ($min && version_compare($unraid['version'], $min, '<')) {
if ($min && version_compare($check_version, $min, '<')) {
write("plugin: installed Unraid version is too low, require at least version $min\n");
if (dirname($plugin_file) == "$boot") {
move($plugin_file, "$boot-error");
@@ -624,7 +690,7 @@ if ($method == 'install') {
done(1);
}
$max = plugin('max', $plugin_file, $error) ?: plugin('Unraid', $plugin_file, $error);
if ($max && version_compare($unraid['version'], $max, '>')) {
if ($max && version_compare($check_version, $max, '>')) {
write("plugin: installed Unraid version is too high, require at most version $max\n");
if (dirname($plugin_file) == "$boot") {
move($plugin_file, "$boot-error");
@@ -659,7 +725,6 @@ if ($method == 'install') {
done(1);
}
// check version installation?
$forced = $nchan ? ($argc==5 ? $argv[4] : false) : ($argc==4 ? $argv[3] : false);
if (!$forced) {
// do not re-install if same plugin already installed or has higher version
if (strcmp($version, $installed_version) < 0) {
@@ -711,11 +776,13 @@ if ($method == 'install') {
if (!plugin('noInstall', $plugin_file, $error)) {
if ($target != $plugin_file) copy($plugin_file, $target);
symlink($target, $symlink);
write("plugin: $plugin installed\n");
my_logger("$plugin installed", $logger);
$status = 'installed';
write("plugin: $plugin $status\n");
my_logger("$plugin $status", $logger);
} else {
write("script: $plugin executed\n");
my_logger("script: $plugin executed", $logger);
$script_action = 'executed';
write("script: $plugin $script_action\n");
my_logger("script: $plugin $script_action", $logger);
}
// run hook scripts for post processing
post_hooks();
@@ -778,7 +845,11 @@ if ($method == 'check') {
if ($method == 'update') {
$plugin = $argv[2];
$symlink = "$plugins/$plugin";
write("plugin: updating: $plugin\n");
if ($download_only) {
write("plugin: download-only mode enabled, skipping install commands\n");
}
$action = $download_only ? 'downloading' : 'updating';
write("plugin: $action: $plugin\n");
$installed_plugin_file = @readlink($symlink);
if ($installed_plugin_file === false) {
write("plugin: $plugin not installed\n");
@@ -794,14 +865,14 @@ if ($method == 'update') {
}
// bergware - check Unraid version dependency (if present)
$min = plugin('min', $plugin_file, $error);
if ($min && version_compare($unraid['version'], $min, '<')) {
if ($min && version_compare($check_version, $min, '<')) {
write("plugin: installed Unraid version is too low, require at least version $min\n");
// run hook scripts for post processing
post_hooks($error);
done(1);
}
$max = plugin('max', $plugin_file, $error) ?: plugin('Unraid', $plugin_file, $error);
if ($max && version_compare($unraid['version'], $max, '>')) {
if ($max && version_compare($check_version, $max, '>')) {
write("plugin: installed Unraid version is too high, require at most version $max\n");
// run hook scripts for post processing
post_hooks($error);
@@ -824,12 +895,17 @@ if ($method == 'update') {
done(1);
}
// install was successful, save the updated plugin so it installs again next boot
unlink($symlink);
$target = "$boot/$plugin";
$target = $download_only ? "$nextboot/$plugin" : "$boot/$plugin";
$status = $download_only ? 'downloaded' : 'updated';
// For normal update, unlink existing symlink before copying
if (!$download_only) unlink($symlink);
// Ensure target directory exists and copy plugin file
@mkdir(dirname($target), 0770, true);
copy($plugin_file, $target);
symlink($target, $symlink);
write("plugin: $plugin updated\n");
my_logger("$plugin updated", $logger);
// For normal update, create symlink to mark as installed
if (!$download_only) symlink($target, $symlink);
write("plugin: $plugin $status\n");
my_logger("$plugin $status", $logger);
// run hook scripts for post processing
post_hooks();
done(0);

View File

@@ -3,6 +3,7 @@ Title="Virtual Machines"
Tag="columns"
Cond="is_file('/var/run/libvirt/libvirtd.pid')"
Markdown="false"
Focus="tableHeaderResize"
---
<?PHP
/* Copyright 2005-2023, Lime Technology
@@ -462,37 +463,43 @@ function loadlist() {
$('.text').click(showInput);
$('.input').blur(hideInput);
<?if (_var($display,'resize')):?>
fillAvailableHeight({
targetElementSelector: '.js-fill-available-height',
elementSelectorsForHeight: [
'.js-actions',
'#kvm_table thead',
],
elementSelectorsForSpacing: [
'#kvm_table',
],
manualSpacingOffset: 50, // without this, the main content will still be scrollable by like 20px
});
// Handle table header fixed positioning after resize
function tableHeaderResize() {
$('#kvm_table thead,#kvm_table tbody').removeClass('fixed');
$('#kvm_table thead tr th').each(function(){$(this).width($(this).width());});
$('#kvm_table tbody tr td').each(function(){$(this).width($(this).width());});
$('#kvm_table thead,#kvm_table tbody').not('.child').addClass('fixed');
}
tableHeaderResize();
$(window).bind('resize',function(){
tableHeaderResize();
});
// Handle table header resizing when tab is clicked
<?if (count($pages) > 2):?>
$("#tab1").on('click',function(){
// Handle table header resizing when window is resized with debouncing
var resizeTimeout;
$(window).off('resize.vmTableResize').on('resize.vmTableResize',function(){
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(function(){
tableHeaderResize();
});
<?endif;?>
<?endif;?>
}, 150);
});
<?endif;?>
});
}
// Handle table header fixed positioning after resize
// Only applicable to listing height: fixed
// Function needs to exist in the global scope due to "Focus" attribute in the page
function tableHeaderResize() {
<?if (_var($display,'resize')):?>
fillAvailableHeight({
targetElementSelector: '.js-fill-available-height',
elementSelectorsForHeight: [
'.js-actions',
'#kvm_table thead',
],
elementSelectorsForSpacing: [
'#kvm_table',
],
manualSpacingOffset: 50, // without this, the main content will still be scrollable by like 20px
});
$('#kvm_table thead,#kvm_table tbody').removeClass('fixed');
$('#kvm_table thead tr th').each(function(){$(this).width($(this).width());});
$('#kvm_table tbody tr td').each(function(){$(this).width($(this).width());});
$('#kvm_table thead,#kvm_table tbody').not('.child').addClass('fixed');
<?endif;?>
}
$(function() {
<?if ($msg):?>
<?$color = strpos($msg, "rror:")!==false ? 'red-text':'green-text'?>

View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1 @@
{}

View File

@@ -104,8 +104,8 @@ $arrConfigDefaults = [
'autoport' => 'yes',
'model' => 'qxl',
'keymap' => 'none',
'port' => -1 ,
'wsport' => -1,
'port' => 5900,
'wsport' => 5901,
'copypaste' => 'no',
'render' => 'auto',
'DisplayOptions' => ""
@@ -256,7 +256,7 @@ if (isset($_POST['updatevm'])) {
$xml = str_replace($olduuid,$newuuid,$xml);
} else {
// form view
if ($error = create_vdisk($_POST) === false) {
if (($error = create_vdisk($_POST)) === false) {
$arrExistingConfig = custom::createArray('domain',$strXML);
$arrUpdatedConfig = custom::createArray('domain',$lv->config_to_xml($_POST));
if ($debug) {
@@ -1327,9 +1327,9 @@ foreach ($arrConfig['shares'] as $i => $arrShare) {
?>
</select></span>
<span id="Porttext" class="label <?=$hiddenport?>">_(VM Console Port)_:</span>
<input id="port" type="number" size="5" maxlength="5" class="trim second <?=$hiddenport?>" name="gpu[<?=$i?>][port]" value="<?=$arrGPU['port']?>">
<input id="port" onchange="checkVNCPorts()" min="5900" max="65535" type="number" size="5" maxlength="5" class="trim second <?=$hiddenport?>" name="gpu[<?=$i?>][port]" value="<?=$arrGPU['port']?>">
<span id="WSPorttext" class="label <?=$hiddenwsport?>">_(VM Console WS Port)_:</span>
<input id="wsport" type="number" size="5" maxlength="5" class="trim second <?=$hiddenwsport?>" name="gpu[<?=$i?>][wsport]" value="<?=$arrGPU['wsport']?>">
<input id="wsport" onchange="checkVNCPorts()" min="5900" max="65535" type="number" size="5" maxlength="5" class="trim second <?=$hiddenwsport?>" name="gpu[<?=$i?>][wsport]" value="<?=$arrGPU['wsport']?>">
</td>
<td></td>
</tr>
@@ -2124,6 +2124,18 @@ foreach ($arrConfig['evdev'] as $i => $arrEvdev) {
var storageType = "<?=get_storage_fstype($arrConfig['template']['storage']);?>";
var storageLoc = "<?=$arrConfig['template']['storage']?>";
function checkVNCPorts() {
const port = $("#port").val();
const wsport = $("#wsport").val();
if (port < 5900 || port > 65535 || wsport < 5900 || wsport > 65535 || port == wsport) {
swal({
title: "_(Invalid Port)_",
text: "_(VNC/SPICE ports must be between 5900 and 65535, and cannot be equal to each other)_",
type: "error",
confirmButtonText: "_(Ok)_"
});
}
}
function updateMAC(index, port) {
var wlan0 = '<?=$mac?>'; // mac address of wlan0
var mac = $('input[name="nic['+index+'][mac]"');
@@ -2635,6 +2647,13 @@ $(function() {
});
<?endif?>
$("#vmform #domain_machine").change(function changeMachineEvent(){
// Cdrom Bus: select IDE for i440 and SATA for q35
if ($(this).val().indexOf('q35') != -1) {
$('#vmform .cdrom_bus').val('sata');
}
});
$("#vmform .domain_vcpu").change(function changeVCPUEvent(){
var $cores = $("#vmform .domain_vcpu:checked");
if ($cores.length < 1) {

36
emhttp/plugins/dynamix/ArrayOperation.page Normal file → Executable file
View File

@@ -348,8 +348,40 @@ function shutdown_now(form, cmd) {
$(form).append('<input type="hidden" name="cmd" value="'+cmd+'">');
<?if ($confirm['down']):?>
switch (cmd) {
case 'reboot': var text = "_(This will reboot the system)_"; break;
case 'shutdown': var text = "_(This will shutdown the system)_"; break;
case 'reboot':
if ($('input[name="safemode"]').prop('checked')) {
<?
$nginx = @parse_ini_file("/var/local/emhttp/nginx.ini") ?: [];
$tailscaleUI = false;
$connectRemote = false;
foreach ($nginx as $key => $value) {
if (strpos($key, 'NGINX_TAILSCALE') !== false && $value == $_SERVER['HTTP_HOST']) {
$tailscaleUI = true;
break;
}
if (strpos($key, 'NGINX_WANFQDN') !== false && $value == $_SERVER['HTTP_HOST']) {
$connectRemote = true;
break;
}
}
?>
var text = "_(This will reboot the server in safe mode.)_\n\n" + "_(No plugins will be loaded in safe mode.)_";
<? if (is_file("/var/log/plugins/tailscale.plg") || is_file("/var/log/plugins/tailscale-preview.plg") || is_file("/var/log/plugins/dynamix.unraid.net.plg")): ?>
text += " _(If you use Tailscale or Unraid Connect Remote Access to access the webGUI or services of the server, this access will be lost.)_";
<? if ($tailscaleUI): ?>
text += "\n\n<span class='strong'>_(You are currently accessing the webGUI of the server via Tailscale.)_</span>";
<? endif; ?>
<? if ($connectRemote): ?>
text += "\n\n<span class='strong'>_(You are currently accessing the webGUI of the server via Unraid Connect Remote Access.)_</span>";
<? endif; ?>
<? endif; ?>
} else {
var text = "_(This will reboot the server.)_";
}
break;
case 'shutdown':
var text = "_(This will shutdown the server.)_";
break;
}
swal({
title:"_(Proceed)_?",

View File

@@ -94,7 +94,7 @@ foreach ($devs as $disk) {
}
$array_percent = number_format(100*$array_used/($array_size ?: 1),1,$dot,'');
exec('cat /sys/devices/system/cpu/*/topology/thread_siblings_list|sort -nu', $cpus);
$cpus=get_cpu_packages();
$wg_up = $wireguard ? exec("wg show interfaces") : '';
$wg_up = $wg_up ? explode(' ',$wg_up) : [];
$up = count($wg_up);
@@ -351,8 +351,13 @@ switch ($themeHelper->getThemeName()) { // $themeHelper set in DefaultPageLayout
<td>
<span class='flex flex-row flex-wrap items-center gap-4'>
<span class="head_info">
<span id='cpu-temp'></span>
<span id='cpu-total-power'><i class='fa fa-fw fa-plug'></i>_(Total)_ _(Power)_: N/A</span>
</span>
<?if (count($cpus)<2):?>
<span class="head_info">
<i class="fa fa-thermometer"></i> _(Temperature)_: <span id='cpu-temp0'>N/A</span>
</span>
<?endif;?>
<span class="switch">
_(Load)_:<span class="head_bar">
<span class='cpu_ load'>0%</span>
@@ -398,22 +403,29 @@ switch ($themeHelper->getThemeName()) { // $themeHelper set in DefaultPageLayout
</span>
</td>
</tr>
<tr>
<?
foreach ($cpus as $pair) {
[$cpu1, $cpu2] = my_preg_split('/[,-]/',$pair);
echo "<tr class='cpu_open'>";
if ($is_intel_cpu && count($core_types) > 0)
$core_type = "({$core_types[$cpu1]})";
else
$core_type = "";
foreach ($cpus as $cpu_index=>$package) {
if (count($cpus) > 1) {
echo "<td><span class='cpu_open w72'><i class='fa fa-plug'></i> "._("Physical")." CPU $cpu_index "._("Power").": <span id='cpu-power$cpu_index'>N/A </span> ";
if (count($cpus)>1) echo "<i class='fa fa-thermometer'></i> "._("Temperature").": <span id='cpu-temp$cpu_index'>N/A</span>";
echo "</td></span></tr>";
}
foreach ($package as $pair) {
[$cpu1, $cpu2] = my_preg_split('/[,-]/',$pair);
echo "<tr class='cpu_open'>";
if ($is_intel_cpu && count($core_types) > 0)
$core_type = "({$core_types[$cpu1]})";
else
$core_type = "";
if ($cpu2)
echo "<td><span class='w26'>CPU $cpu1 $core_type - HT $cpu2 </span><span class='dashboard w36'><span class='cpu$cpu1 load resize'>0%</span><div class='usage-disk sys'><span id='cpu$cpu1'></span><span></span></div></span><span class='dashboard w36'><span class='cpu$cpu2 load resize'>0%</span><div class='usage-disk sys'><span id='cpu$cpu2'></span><span></span></div></span></td>";
else
echo "<td><span class='w26'>CPU $cpu1 $core_type</span><span class='w72'><span class='cpu$cpu1 load resize'>0%</span><div class='usage-disk sys'><span id='cpu$cpu1'></span><span></span></div></span></td>";
echo "</tr>";
}
if ($cpu2)
echo "<td><span class='w26'>CPU $cpu1 $core_type - HT $cpu2 </span><span class='dashboard w36'><span class='cpu$cpu1 load resize'>0%</span><div class='usage-disk sys'><span id='cpu$cpu1'></span><span></span></div></span><span class='dashboard w36'><span class='cpu$cpu2 load resize'>0%</span><div class='usage-disk sys'><span id='cpu$cpu2'></span><span></span></div></span></td>";
else
echo "<td><span class='w26'>CPU $cpu1 $core_type</span><span class='w72'><span class='cpu$cpu1 load resize'>0%</span><div class='usage-disk sys'><span id='cpu$cpu1'></span><span></span></div></span></td>";
echo "</tr>";
}
}
?>
<tr id='cpu_chart'>
<td>
@@ -1441,6 +1453,7 @@ var startup = true;
var stopgap = '<thead class="stopgap"><tr><td class="stopgap"></td></tr></thead>';
var recall = null;
var recover = null;
var tempunit="<?=_var($display,'unit','C');?>";
// Helper function to calculate millisPerPixel based on container width
function getMillisPerPixel(timeInSeconds, containerId) {
@@ -1695,6 +1708,39 @@ function addChartNet(rx, tx) {
txTimeSeries.append(now, Math.floor(tx / 1000));
}
function updateCPUPower() {
if (!cpupower) return;
// Update total power
const totalEl = document.getElementById('cpu-total-power');
const totalPower = cpupower.totalPower ?? 0;
if (totalEl) {
totalEl.innerHTML = `<i class="fa fa-fw fa-plug"></i> _(Total)_ _(Power)_: ${totalPower.toFixed(2)} W`;
}
// Update each core's span
const cpuspower = cpupower.power ?? [];
cpuspower.forEach((power, index) => {
const coreEl = document.getElementById(`cpu-power${index}`);
if (coreEl) {
coreEl.innerHTML = `${power.toFixed(2)} W`;
}
});
const cpustemps = cpupower.temp ?? [];
cpustemps.forEach((temp, index) => {
const coreTempEl = document.getElementById(`cpu-temp${index}`);
if (coreTempEl) {
tempdisplay = temp.toFixed(0);
if (tempunit === "F") {
tempdisplay = ((temp.toFixed(0))* 9 / 5) + 32;
}
coreTempEl.innerHTML = Math.round(tempdisplay)+`&#8201;&#176;`+tempunit;;
}
});
}
// Cache for last values to avoid unnecessary DOM updates
var lastCpuValues = {
load: -1,
@@ -2770,6 +2816,60 @@ $(function() {
setTimeout(function() {
// Charts initialized
},500);
// Start GraphQL CPU power subscription with retry logic
let cpuInitPWRAttempts = 0, cpuPWRRetryMs = 100;
function initPwrCpuSubscription() {
if (window.gql && window.apolloClient) {
// Define the subscription query when GraphQL is available
// corepower has the temps currently.
CPU_POWER_SUBSCRIPTION = window.gql(`
subscription SystemMetricsCpuTelemetry {
systemMetricsCpuTelemetry {
totalPower,
power,
temp,
}
}
`);
cpuPowerSubscription = window.apolloClient.subscribe({
query: CPU_POWER_SUBSCRIPTION
}).subscribe({
next: (result) => {
if (result.data?.systemMetricsCpuTelemetry){
cpupower = result.data.systemMetricsCpuTelemetry;
updateCPUPower();
}
},
error: (err) => {
console.error('CPU power subscription error:', err);
// Try to resubscribe with capped backoff
if (cpuPowerSubscription) { try { cpuPowerSubscription.unsubscribe(); } catch(e){} }
setTimeout(initPwrCpuSubscription, Math.min(cpuPWRRetryMs *= 2, 5000));
}
});
} else {
// Retry with capped backoff if GraphQL client not ready
cpuInitPWRAttempts++;
setTimeout(initPwrCpuSubscription, Math.min(cpuPWRRetryMs *= 2, 2000));
}
}
initPwrCpuSubscription();
// Cleanup GraphQL subscription on page unload
$(window).on('beforeunload', function() {
if (cpuPowerSubscription) {
cpuPowerSubscription.unsubscribe();
}
});
// Cleanup GraphQL subscription on page unload
$(window).on('beforeunload', function() {

View File

@@ -344,9 +344,9 @@ function selectDiskFsWidthZFS(slots,init) {
value: width,
text: _(sprintf('%s '+label+' of %s devices',groups,width)),
}));
if (selected_width == 0) selected_width = width;
}
}
selected_width = slots;
}
$('#diskFsWidth').val(selected_width);
}
@@ -990,7 +990,7 @@ _(Critical disk utilization threshold)_ (%):
</form>
<?if (fsType('btrfs')):?>
<?if (!maintenance_mode()):?>
<?if (_var($disk,'fsStatus')=="Mounted"):?>
<div class="title nocontrol">
<span class="left">
<i class="title fa fa-hdd-o"></i> _(Pool Device Status)_

View File

@@ -15,6 +15,8 @@ Tag="calendar-check-o"
*/
?>
<?
$mode = ['Disabled','Hourly','Daily','Weekly','Monthly'];
$days = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];
$setup = true;
if (!$pool_devices) {
echo "<p class='notice'>"._('No Cache device present')."!</p>";
@@ -23,11 +25,13 @@ if (!$pool_devices) {
echo "<p class='notice'>"._('User shares not enabled')."!</p>";
$setup = false;
}
$cron = explode(' ',$var['shareMoverSchedule']);
$move = $cron[2]!='*' ? 3 : ($cron[4]!='*' ? 2 : (substr($cron[1],0,1)!='*' ? 1 : 0));
$mode = ['Hourly','Daily','Weekly','Monthly'];
$days = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];
if (empty($var['shareMoverSchedule'])) {
$cron = explode(' ', "* * * * *");
$move = 0;
} else {
$cron = explode(' ', $var['shareMoverSchedule']);
$move = $cron[2]!='*' ? 4 : ($cron[4]!='*' ? 3 : (substr($cron[1],0,1)!='*' ? 2 : 1));
}
$showMoverButton = $setup && $pool_devices;
$moverRunning = file_exists('/var/run/mover.pid');
?>
@@ -37,27 +41,33 @@ $(function() {
presetMover(document.mover_schedule);
});
<? endif; ?>
function presetMover(form) {
var mode = form.shareMoverSchedule.value;
form.min.disabled = mode==0;
form.day.disabled = mode==0 || mode!=3;
form.dotm.disabled = mode==0 || mode!=4;
form.hour1.disabled = mode==0;
form.hour2.disabled = mode==0;
form.day.value = form.day.disabled ? '*' : (form.day.value=='*' ? 0 : form.day.value);
form.dotm.value = form.dotm.disabled ? '*' : (form.dotm.value=='*' ? 1 : form.dotm.value);
if (mode==1) {$('#H1').hide(); $('#H2').show();} else {$('#H2').hide(); $('#H1').show();}
}
// Fool Unraid by simulating the original input field
function prepareMover(form) {
var mode = form.shareMoverSchedule.value;
var min = mode!=0 ? form.min.value : 0;
var hour = mode!=0 ? form.hour1.value : form.hour2.value;
form.shareMoverSchedule.options[mode].value = min+' '+hour+' '+form.dotm.value+' * '+form.day.value;
if (mode == 0)
form.shareMoverSchedule.options[mode].value = '';
else {
var hour = mode!=1 ? form.hour1.value : form.hour2.value;
var min = mode!=1 ? form.min.value : 0;
form.shareMoverSchedule.options[mode].value = min+' '+hour+' '+form.dotm.value+' * '+form.day.value;
}
form.min.disabled = true;
form.hour1.disabled = true;
form.hour2.disabled = true;
form.dotm.disabled = true;
form.day.disabled = true;
}
function presetMover(form) {
var mode = form.shareMoverSchedule.value;
form.min.disabled = false;
form.day.disabled = mode!=2;
form.dotm.disabled = mode!=3;
form.day.value = form.day.disabled ? '*' : (form.day.value=='*' ? 0 : form.day.value);
form.dotm.value = form.dotm.disabled ? '*' : (form.dotm.value=='*' ? 1 : form.dotm.value);
if (mode==0) {$('#H1').hide(); $('#H2').show();} else {$('#H2').hide(); $('#H1').show();}
}
</script>
<form markdown="1" name="mover_schedule" method="POST" action="/update.htm" target="progressFrame" onsubmit="prepareMover(this)">
<?if ($setup):?>
@@ -92,7 +102,7 @@ _(Day of the month)_:
_(Time of the day)_:
: <span>
<span id="H1"<?if ($move==0):?> style="display:none"<?endif;?>><select name="hour1" class="narrow">
<span id="H1"<?if ($move==1):?> style="display:none"<?endif;?>><select name="hour1" class="narrow">
<?for ($d=0; $d<=23; $d++):?>
<?=mk_option($cron[1], strval($d), sprintf("%02d", $d))?>
<?endfor;?>
@@ -102,7 +112,7 @@ _(Time of the day)_:
<?=mk_option($cron[0], strval($d), sprintf("%02d", $d))?>
<?endfor;?>
</select>&nbsp;&nbsp;_(HH:MM)_</span>
<span id="H2"<?if ($move!=0):?> style="display:none"<?endif;?>><select name="hour2">
<span id="H2"<?if ($move!=1):?> style="display:none"<?endif;?>><select name="hour2">
<?=mk_option($cron[1], "*/1", _("Every hour"))?>
<?=mk_option($cron[1], "*/2", _("Every 2 hours"))?>
<?=mk_option($cron[1], "*/3", _("Every 3 hours"))?>

View File

@@ -1,9 +1,10 @@
Menu="Notifications:3"
Title="Notification Agents"
Tag="rss-square"
Focus="initDropdown"
---
<?php
/* Copyright 2005-2023, Lime Technology
/* Copyright 2005-2025, Lime Technology
* Copyright 2012-2023, Bergware International.
*
* This program is free software; you can redistribute it and/or
@@ -21,12 +22,14 @@ var disabledAgents = new Object();
var openPage = true;
<?$width = [166,300]?>
<?if ($tabbed):?>
$('#tab3').bind({click:function(){initDropdown();}});
$(function(){if ($('#tab3').is(':checked')) initDropdown();});
<?else:?>
<? if (!$tabbed): ?>
$(function(){initDropdown();});
<?endif;?>
<? else: ?>
$(function(){
if ( $('#tab3').attr('checked') ) initDropdown();
});
<? endif; ?>
if (!String.prototype.format) {
String.prototype.format = function() {

View File

@@ -214,7 +214,7 @@ function writeNFS(data, n, i) {
var data = [];
/* Get the setting from the share config. */
var hostList = $('textarea[name="shareHostListNFS"]').val().trim();
var hostList = $('textarea[name="shareHostListNFS"]').val()?.trim() || "";
/* Replace all new lines in data.hostList with spaces. */
var formattedHostList = <?= json_encode($sec_nfs[$name]['hostList']); ?>.replace(/\n/g, ' ');

View File

@@ -0,0 +1,25 @@
<Agent>
<Name>Join</Name>
<Variables>
<Variable Help="The API key can be found [a href='https://joinjoaomgcd.appspot.com' target='_blank'] [u]here[/u].[/a]" Desc="API key" Default="">API_KEY</Variable>
<Variable Help="Specify the fields which are included in the title of the notification." Desc="Notification Title" Default="$SUBJECT">TITLE</Variable>
<Variable Help="Specify the fields which are included in the message body of the notification." Desc="Notification Message" Default="$DESCRIPTION">MESSAGE</Variable>
</Variables>
<Script>
<![CDATA[
#!/bin/bash
##########
{0}
##########
TITLE=$(echo -e "$TITLE")
MESSAGE=$(echo -e "$MESSAGE")
curl -s -k -G \
-d "apikey=$API_KEY" \
--data-urlencode "title=$TITLE" \
--data-urlencode "text=$MESSAGE" \
-d "deviceId=group.all" \
https://joinjoaomgcd.appspot.com/_ah/api/messaging/v1/sendPush 2>&1
]]>
</Script>
</Agent>

View File

@@ -1,5 +1,7 @@
<div id="header" class="<?=$display['banner']?>">
<unraid-header-os-version></unraid-header-os-version>
<? if ($display['usage'] && $themeHelper->isSidebarTheme()): ?>
<span id='array-usage-sidenav'></span>
<? endif; ?>
<?include "$docroot/plugins/dynamix.my.servers/include/myservers2.php"?>
</div>

View File

@@ -37,10 +37,12 @@
</div>
<? endif; ?>
<? if ($display['usage']): ?>
<? if ($display['usage'] && ! $themeHelper->isSidebarTheme()): ?>
<? my_usage(); ?>
<? endif; ?>
<? if ($display['usage'] && $themeHelper->isSidebarTheme()): ?>
<script>$("#array-usage-sidenav").html("<?my_usage();?>");</script>
<? endif; ?>
<? foreach ($buttonPages as $button): ?>
<? if (empty($button['Link'])): ?>
<? $icon = $button['Icon']; ?>

View File

@@ -565,10 +565,8 @@ function dmidecode($key, $n, $all=true) {
}
function is_intel_cpu() {
$cpu = dmidecode('Processor Information','4',0);
$cpu_vendor = $cpu['Manufacturer'] ?? "";
$is_intel_cpu = stripos($cpu_vendor, "intel") !== false ? true : false;
return $is_intel_cpu;
$cpu_vendor_check = exec("grep -Pom1 '^model name\s+:\s*\K.+' /proc/cpuinfo") ?? "";
return stripos($cpu_vendor_check, "intel") !== false;
}
// Load saved PCI data
@@ -874,7 +872,6 @@ function get_cpu_packages(string $separator = ','): array {
return $packages;
}
function getIpAddressesByPci(string $pciAddress): array
{
$base = "/sys/bus/pci/devices/$pciAddress/net";

View File

@@ -0,0 +1,22 @@
<?php
function get_cpu_packages(string $separator = ','): array {
$packages = [];
foreach (glob("/sys/devices/system/cpu/cpu[0-9]*/topology/thread_siblings_list") as $path) {
$pkg_id = (int)file_get_contents(dirname($path) . "/physical_package_id");
$siblings = str_replace(",", $separator, trim(file_get_contents($path)));
if (!in_array($siblings, $packages[$pkg_id] ?? [])) {
$packages[$pkg_id][] = $siblings;
}
}
// Sort groups within each package by first CPU number
foreach ($packages as &$list) {
$keys = array_map(fn($s) => (int)explode($separator, $s)[0], $list);
array_multisort($keys, SORT_ASC, SORT_NUMERIC, $list);
}
unset($list);
return $packages;
}

View File

@@ -497,7 +497,7 @@ run("lscpu 2>/dev/null|todos >".escapeshellarg("/$diag/system/lscpu.txt"));
run("lsscsi -vgl 2>/dev/null|todos >".escapeshellarg("/$diag/system/lsscsi.txt"));
run("lspci -knn 2>/dev/null|todos >".escapeshellarg("/$diag/system/lspci.txt"));
run("lspci -vv 2>/dev/null| awk -b '/ASPM/{print $0}' RS=|grep -P '(^[a-z0-9:.]+|ASPM |Disabled;|Enabled;)'|todos >".escapeshellarg("/$diag/system/aspm-status.txt"));
run("lsusb 2>/dev/null|todos >".escapeshellarg("/$diag/system/lsusb.txt"));
run("lsusb -vt 2>/dev/null|todos >".escapeshellarg("/$diag/system/lsusb.txt"));
run("free -mth 2>/dev/null|todos >".escapeshellarg("/$diag/system/memory.txt"));
run("lsof -Pni 2>/dev/null|todos >".escapeshellarg("/$diag/system/lsof.txt"));
run("lsmod|sort 2>/dev/null|todos >".escapeshellarg("/$diag/system/lsmod.txt"));

View File

@@ -104,6 +104,37 @@ function clean_subject($subject) {
return $subject;
}
/**
* Wrap string values in double quotes for INI compatibility and escape quotes/backslashes.
* Numeric types remain unquoted so they can be parsed as-is.
*/
function ini_encode_value($value) {
if (is_int($value) || is_float($value)) return $value;
if (is_bool($value)) return $value ? 'true' : 'false';
$value = (string)$value;
return '"'.strtr($value, ["\\"=>"\\\\", '"' => '\\"']).'"';
}
function build_ini_string(array $data) {
$lines = [];
foreach ($data as $key => $value) {
$lines[] = "{$key}=".ini_encode_value($value);
}
return implode("\n", $lines)."\n";
}
/**
* Trims and unescapes strings (eg quotes, backslashes) if necessary.
*/
function ini_decode_value($value) {
$value = trim($value);
$length = strlen($value);
if ($length >= 2 && $value[0] === '"' && $value[$length-1] === '"') {
return stripslashes(substr($value, 1, -1));
}
return $value;
}
// start
if ($argc == 1) exit(usage());
@@ -222,10 +253,29 @@ case 'add':
$archive = "{$archive}/".safe_filename("{$event}-{$ticket}.notify");
if (file_exists($archive)) break;
$entity = $overrule===false ? $notify[$importance] : $overrule;
if (!$mailtest) file_put_contents($archive,"timestamp=$timestamp\nevent=$event\nsubject=$subject\ndescription=$description\nimportance=$importance\n".($message ? "message=".str_replace('\n','<br>',$message)."\n" : ""));
if (($entity & 1)==1 && !$mailtest && !$noBrowser) file_put_contents($unread,"timestamp=$timestamp\nevent=$event\nsubject=$subject\ndescription=$description\nimportance=$importance\nlink=$link\n");
if (($entity & 2)==2 || $mailtest) generate_email($event, clean_subject($subject), str_replace('<br>','. ',$description), $importance, $message, $recipients, $fqdnlink);
if (($entity & 4)==4 && !$mailtest) { if (is_array($agents)) {foreach ($agents as $agent) {exec("TIMESTAMP='$timestamp' EVENT=".escapeshellarg($event)." SUBJECT=".escapeshellarg(clean_subject($subject))." DESCRIPTION=".escapeshellarg($description)." IMPORTANCE=".escapeshellarg($importance)." CONTENT=".escapeshellarg($message)." LINK=".escapeshellarg($fqdnlink)." bash ".$agent);};}};
$cleanSubject = clean_subject($subject);
$archiveData = [
'timestamp' => $timestamp,
'event' => $event,
'subject' => $cleanSubject,
'description' => $description,
'importance' => $importance,
];
if ($message) $archiveData['message'] = str_replace('\n','<br>',$message);
if (!$mailtest) file_put_contents($archive, build_ini_string($archiveData));
if (($entity & 1)==1 && !$mailtest && !$noBrowser) {
$unreadData = [
'timestamp' => $timestamp,
'event' => $event,
'subject' => $cleanSubject,
'description' => $description,
'importance' => $importance,
'link' => $link,
];
file_put_contents($unread, build_ini_string($unreadData));
}
if (($entity & 2)==2 || $mailtest) generate_email($event, $cleanSubject, str_replace('<br>','. ',$description), $importance, $message, $recipients, $fqdnlink);
if (($entity & 4)==4 && !$mailtest) { if (is_array($agents)) {foreach ($agents as $agent) {exec("TIMESTAMP='$timestamp' EVENT=".escapeshellarg($event)." SUBJECT=".escapeshellarg($cleanSubject)." DESCRIPTION=".escapeshellarg($description)." IMPORTANCE=".escapeshellarg($importance)." CONTENT=".escapeshellarg($message)." LINK=".escapeshellarg($fqdnlink)." bash ".$agent);};}};
break;
case 'get':
@@ -241,9 +291,12 @@ case 'get':
$output[$i]['show'] = (fileperms($file) & 0x0FFF)==0400 ? 0 : 1;
foreach ($fields as $field) {
if (!$field) continue;
[$key,$val] = array_pad(explode('=', $field),2,'');
# limit the explode('=', …) used during reads to two pieces so values containing = remain intact
[$key,$val] = array_pad(explode('=', $field, 2),2,'');
if ($time) {$val = date($notify['date'].' '.$notify['time'], $val); $time = false;}
$output[$i][trim($key)] = trim($val);
# unescape the value before emitting JSON, so the browser UI
# and any scripts calling `notify get` still see plain strings
$output[$i][trim($key)] = ini_decode_value($val);
}
$i++;
}

View File

@@ -139,15 +139,15 @@ hr {
resize: none;
}
input[type="text"],
input[type="password"],
input[type="number"],
input[type="url"],
input[type="email"],
input[type="date"],
input[type="file"],
textarea,
.textarea {
input[type="text"]:where(:not(.unapi *)),
input[type="password"]:where(:not(.unapi *)),
input[type="number"]:where(:not(.unapi *)),
input[type="url"]:where(:not(.unapi *)),
input[type="email"]:where(:not(.unapi *)),
input[type="date"]:where(:not(.unapi *)),
input[type="file"]:where(:not(.unapi *)),
textarea:where(:not(.unapi *)),
.textarea:where(:not(.unapi *)) {
color: var(--text-color);
font-family: clear-sans;
font-size: 1.3rem;
@@ -181,12 +181,12 @@ select:focus {
}
}
input[type="button"],
input[type="reset"],
input[type="submit"],
button,
button[type="button"],
a.button {
input[type="button"]:where(:not(.unapi *)),
input[type="reset"]:where(:not(.unapi *)),
input[type="submit"]:where(:not(.unapi *)),
button:where(:not(.unapi *)),
button[type="button"]:where(:not(.unapi *)),
a.button:where(:not(.unapi *)) {
font-family: clear-sans;
font-size: 1.1rem;
font-weight: bold;
@@ -250,48 +250,50 @@ input[type="number"]::-webkit-inner-spin-button {
input[type="number"] {
-moz-appearance: textfield;
}
input:focus[type="text"],
input:focus[type="password"],
input:focus[type="number"],
input:focus[type="url"],
input:focus[type="email"],
input:focus[type="file"],
textarea:focus {
input:focus[type="text"]:where(:not(.unapi *)),
input:focus[type="password"]:where(:not(.unapi *)),
input:focus[type="number"]:where(:not(.unapi *)),
input:focus[type="url"]:where(:not(.unapi *)),
input:focus[type="email"]:where(:not(.unapi *)),
input:focus[type="file"]:where(:not(.unapi *)),
textarea:focus:where(:not(.unapi *)) {
background-color: var(--focus-input-bg-color);
outline: 0;
}
input:hover[type="button"],
input:hover[type="reset"],
input:hover[type="submit"],
button:hover,
button:hover[type="button"],
a.button:hover {
input:hover[type="button"]:where(:not(.unapi *)),
input:hover[type="reset"]:where(:not(.unapi *)),
input:hover[type="submit"]:where(:not(.unapi *)),
button:hover:where(:not(.unapi *)),
button:hover[type="button"]:where(:not(.unapi *)),
a.button:hover:where(:not(.unapi *)) {
color: var(--hover-button-text-color);
background: var(--hover-button-background);
}
input[disabled],
textarea[disabled] {
input[disabled]:where(:not(.unapi *)),
textarea[disabled]:where(:not(.unapi *)) {
color: var(--text-color);
border-bottom-color: var(--disabled-input-border-color);
opacity: 0.5;
cursor: default;
}
input[type="button"][disabled],
input[type="reset"][disabled],
input[type="submit"][disabled],
button[disabled],
button[type="button"][disabled],
a.button[disabled] input:hover[type="button"][disabled],
input:hover[type="reset"][disabled],
input:hover[type="submit"][disabled],
button:hover[disabled],
button:hover[type="button"][disabled],
a.button:hover[disabled] input:active[type="button"][disabled],
input:active[type="reset"][disabled],
input:active[type="submit"][disabled],
button:active[disabled],
button:active[type="button"][disabled],
a.button:active[disabled] {
input[type="button"][disabled]:where(:not(.unapi *)),
input[type="reset"][disabled]:where(:not(.unapi *)),
input[type="submit"][disabled]:where(:not(.unapi *)),
button[disabled]:where(:not(.unapi *)),
button[type="button"][disabled]:where(:not(.unapi *)),
a.button[disabled]:where(:not(.unapi *)),
input:hover[type="button"][disabled]:where(:not(.unapi *)),
input:hover[type="reset"][disabled]:where(:not(.unapi *)),
input:hover[type="submit"][disabled]:where(:not(.unapi *)),
button:hover[disabled]:where(:not(.unapi *)),
button:hover[type="button"][disabled]:where(:not(.unapi *)),
a.button:hover[disabled]:where(:not(.unapi *)),
input:active[type="button"][disabled]:where(:not(.unapi *)),
input:active[type="reset"][disabled]:where(:not(.unapi *)),
input:active[type="submit"][disabled]:where(:not(.unapi *)),
button:active[disabled]:where(:not(.unapi *)),
button:active[type="button"][disabled]:where(:not(.unapi *)),
a.button:active[disabled]:where(:not(.unapi *)) {
opacity: 0.5;
cursor: default;
color: var(--disabled-text-color);
@@ -333,7 +335,7 @@ a.button:active[disabled] {
input::-webkit-input-placeholder {
color: var(--link-text-color);
}
select {
select:where(:not(.unapi *)) {
-webkit-appearance: none;
font-family: clear-sans;
font-size: 1.3rem;
@@ -356,18 +358,18 @@ select {
display: inline-block;
cursor: pointer;
}
select option {
select:where(:not(.unapi *)) option {
color: var(--text-color);
background-color: var(--mild-background-color);
}
select option:disabled {
select:where(:not(.unapi *)) option:disabled {
color: var(--disabled-text-color);
}
select:focus {
select:focus:where(:not(.unapi *)) {
background-color: var(--focus-input-bg-color);
outline: 0;
}
select[disabled] {
select[disabled]:where(:not(.unapi *)) {
color: var(--text-color);
border-bottom-color: var(--disabled-border-color);
opacity: 0.5;
@@ -414,7 +416,7 @@ input.trim {
width: 76px;
min-width: 76px;
}
textarea {
textarea:where(:not(.unapi *)) {
resize: none;
padding: 6px;
border: 1px solid var(--textarea-border-color);
@@ -951,7 +953,7 @@ div.title span img {
-webkit-overflow-scrolling: touch;
}
table {
table:where(:not(.unapi *)) {
border-collapse: collapse;
border-spacing: 0;
border-style: hidden;
@@ -959,12 +961,12 @@ table {
width: 100%;
background-color: var(--background-color);
}
table thead td {
table:where(:not(.unapi *)) thead td {
line-height: 2.8rem;
height: 2.8rem;
white-space: nowrap;
}
table tbody td {
table:where(:not(.unapi *)) tbody td {
line-height: 2.6rem;
height: 2.6rem;
white-space: nowrap;
@@ -2563,7 +2565,7 @@ div#title.ud {
padding: 0;
}
.LanguageButton {
.Theme--nav-top .LanguageButton {
font-size: 12px !important;
} /* Fix Switch Language Being Cut-Off */
@@ -2594,8 +2596,10 @@ div#title.ud {
}
.usage-bar {
position: relative;
width: 6.4rem;
position: absolute;
bottom: .5rem;
left: 50%;
width: 14rem;
margin: 0;
padding: 0;
}

View File

@@ -31,9 +31,6 @@ if [[ -x /etc/rc.d/rc.setterm ]]; then
/etc/rc.d/rc.setterm
fi
# Set the hostname:
hostname $(cat /etc/HOSTNAME)
# Set the permissions on /var/log/dmesg according to whether the kernel
# permits non-root users to access kernel dmesg information:
if [[ -r /proc/sys/kernel/dmesg_restrict ]]; then
@@ -246,7 +243,8 @@ fi
# Start avahi:
if [[ -x /etc/rc.d/rc.avahidaemon ]]; then
/etc/rc.d/rc.avahidaemon start
/etc/rc.d/rc.avahidnsconfd start
# disable by default, users can start manually if needed
# /etc/rc.d/rc.avahidnsconfd start
fi
# Start Samba (a file/print server for Windows machines).

View File

@@ -12,6 +12,14 @@
# run & log functions
. /etc/rc.d/rc.runlog
# LimeTech - restore machine-id
SRC="/boot/config/machine-id"
DEST="/etc/machine-id"
if [[ ! -f "$SRC" ]] || ! grep -Eq '^[0-9a-f]{32}$' "$SRC" ; then
/usr/bin/dbus-uuidgen --ensure="$SRC"
fi
/usr/bin/install -m 0644 "$SRC" "$DEST"
# LimeTech - bind selected devices to vfio-pci
/usr/local/sbin/vfio-pci 1>/var/log/vfio-pci 2>/var/log/vfio-pci-errors
@@ -217,9 +225,10 @@ if [[ -r /boot/config/ident.cfg ]]; then
. <(/usr/bin/fromdos </boot/config/ident.cfg)
NAME=${NAME//[^a-zA-Z\-\.0-9]/\-}
fi
/bin/echo "$NAME" >/etc/HOSTNAME
/bin/hostname "$NAME"
/bin/hostname -s >/etc/hostname
/bin/echo "# Generated" >/etc/hosts
/bin/echo "127.0.0.1 $NAME localhost" >>/etc/hosts
/bin/echo "127.0.0.1 $NAME localhost" >>/etc/hosts
# LimeTech - restore the configured timezone
if [[ $timeZone == custom ]]; then

View File

@@ -31,7 +31,7 @@ CALLER="avahi"
AVAHI="/usr/sbin/avahi-daemon"
CONF="/etc/avahi/avahi-daemon.conf"
HOSTS="/etc/hosts"
NAME=$(</etc/HOSTNAME)
NAME=$(hostname -s)
# run & log functions
. /etc/rc.d/rc.runlog
@@ -51,19 +51,19 @@ disable(){
sed -ri "s/^#?(use-$1)=.*/\1=no/" $CONF
}
# when starting avahidaemon, add name.local to the hosts file
# add name.local to the hosts file
add_local_to_hosts(){
local OLD="^127\.0\.0\.1.*"
local NEW="127.0.0.1 $NAME $NAME.local localhost"
sed -i "s/$OLD/$NEW/gm;t" $HOSTS
local NEW="127.0.0.1 $NAME $NAME.local localhost"
sed -i "s/$OLD/$NEW/" $HOSTS
return 0
}
# when stopping avahidaemon, remove name.local from the hosts file
# remove name.local from the hosts file
remove_local_from_hosts(){
local OLD="^127\.0\.0\.1.*"
local NEW="127.0.0.1 $NAME localhost"
sed -i "s/$OLD/$NEW/gm;t" $HOSTS
local NEW="127.0.0.1 $NAME localhost"
sed -i "s/$OLD/$NEW/" $HOSTS
return 0
}
@@ -113,7 +113,10 @@ avahid_restart(){
}
avahid_reload(){
$AVAHI --reload 2>/dev/null
if avahid_running; then
add_local_to_hosts
$AVAHI --reload 2>/dev/null
fi
}
avahid_update(){

View File

@@ -243,6 +243,8 @@ container_add_route(){
docker_network_start(){
log "Starting network..."
# ensure br_netfilter modules is loaded, re: https://github.com/moby/moby/issues/48948
modprobe br_netfilter
# create list of possible custom networks
EXCLUDE=; INCLUDE=$(ls --indicator-style=none $SYSTEM | awk '/^br[0-9]+/' ORS=' ')
while IFS=$'\n' read -r NETWORK; do
@@ -482,11 +484,11 @@ docker_network_start(){
fi
fi
done
# create IPv6 forward accept rule
if [[ $IPV6_FORWARD == accept ]]; then
ip6tables -P FORWARD ACCEPT
log "created forward accept rule for IPv6 network"
fi
# # create IPv6 forward accept rule
# if [[ $IPV6_FORWARD == accept ]]; then
# ip6tables -P FORWARD ACCEPT
# log "created forward accept rule for IPv6 network"
# fi
log "Network started."
}
@@ -573,6 +575,9 @@ docker_service_start(){
fi
nohup $UNSHARE --propagation slave -- $DOCKER -p $DOCKER_PIDFILE $DOCKER_OPTS >>$DOCKER_LOG 2>&1 &
wait_daemon
# after docket started, continue to accept non-docker IPv6 traffic on br0
ip6tables -P FORWARD ACCEPT
# log "created forward accept rule for IPv6 network"
if docker_running; then REPLY="Started"; else REPLY="Failed"; fi
fi
log "$DAEMON... $REPLY."

View File

@@ -190,6 +190,26 @@ else
log "Installing /boot/extra packages"
( export -f log; find /boot/extra -maxdepth 1 -type f -exec sh -c 'upgradepkg --terse --install-new "$1" | log' -- "{}" \; )
fi
# Move any downloaded plugins in $CONFIG/plugins-nextboot to $CONFIG/plugins so they can be installed
if [[ -d "$CONFIG/plugins-nextboot" ]]; then
shopt -s nullglob
for ITEM in "$CONFIG/plugins-nextboot"/*; do
if [[ -f "$ITEM" && "$ITEM" == *.plg ]]; then
# Move .plg files
mv "$ITEM" "$CONFIG/plugins/" && log "Moved $(basename "$ITEM") from plugins-nextboot to plugins"
elif [[ -d "$ITEM" ]]; then
# Merge plugin directories
PLUGIN_NAME=$(basename "$ITEM")
DEST_DIR="$CONFIG/plugins/$PLUGIN_NAME"
mkdir -p "$DEST_DIR"
cp -r "$ITEM"/* "$DEST_DIR/" 2>/dev/null
rm -rf "$ITEM"
log "Moved plugin directory $PLUGIN_NAME and contents from plugins-nextboot to plugins"
fi
done
shopt -u nullglob
rmdir "$CONFIG/plugins-nextboot" 2>/dev/null;
fi
PRIORITY_PLUGINS=("dynamix.unraid.net.plg")
# Install priority plugins first
for PRIORITY_PLUGIN in "${PRIORITY_PLUGINS[@]}"; do

View File

@@ -155,12 +155,17 @@ samba_stop(){
REPLY="Already stopped"
else
REPLY="Stopped"
# stop gracefully with SIGTERM the "master" smbd process first
master=$(pgrep --ns $$ -o smbd)
[[ -n "$master" ]] && run kill "$master"
sleep 1
# stop gracefully with SIGTERM
run killall --ns $$ smbd nmbd wsdd2 winbindd
samba_waitfor_shutdown
if samba_running; then
REPLY="Killed"
# stop forcibly with SIGKILL
[[ -n "$master" ]] && run kill -SIGKILL "$master"
run killall --ns $$ -SIGKILL smbd nmbd wsdd2 winbindd
samba_waitfor_shutdown
fi

View File

@@ -28,21 +28,21 @@ CFGPATH="/boot/config/shares"
DEBUGGING=""
move() {
find "$1" -depth 2>/dev/null | /usr/libexec/unraid/move $2 $DEBUGGING
find "$1" -depth 2>/dev/null | /usr/libexec/unraid/move $DEBUGGING
# second pass to clean up leftover empty directories
find "$1" -depth -type d 2>/dev/null | /usr/libexec/unraid/move $2 $DEBUGGING
find "$1" -depth -type d 2>/dev/null | /usr/libexec/unraid/move $DEBUGGING
}
start() {
if [ -f $PIDFILE ]; then
if ps h $(cat $PIDFILE) | grep mover ; then
echo "mover: already running"
echo "mover: already running" >&2
exit 1
fi
fi
echo $$ >/var/run/mover.pid
echo "mover: started"
echo "mover: started" >&2
shopt -s nullglob
@@ -92,7 +92,7 @@ start() {
for SHAREPATH in /mnt/$DISK/* ; do
SHARE=$(basename "$SHAREPATH")
if [[ -d "$SHAREPATH" && -f "$CFGPATH/$SHARE.cfg" ]]; then
move "$SHAREPATH" "-e"
move "$SHAREPATH"
fi
done
@@ -108,44 +108,7 @@ start() {
done
rm -f $PIDFILE
echo "mover: finished"
}
empty() {
DISK="$1"
if [ -f $PIDFILE ]; then
if ps h $(cat $PIDFILE) | grep mover ; then
echo "mover: already running"
exit 1
fi
fi
echo $$ >/var/run/mover.pid
echo "mover: started"
shopt -s nullglob
# we can only empty share directories
for SHAREPATH in /mnt/$DISK/* ; do
SHARE=$(basename "$SHAREPATH")
if [[ -d "$SHAREPATH" && -f "$CFGPATH/$SHARE.cfg" ]]; then
move "$SHAREPATH" "-e"
fi
done
# output list of files which could not be moved
# use 'find' in case huge number of files left in /mnt/$DISK
count=$(find /mnt/$DISK -mindepth 1 | wc -l)
if [ "$count" -gt 0 ]; then
find /mnt/$DISK -mindepth 1 -depth -printf 'move: %p Not moved\n' | head -n 100
if [ "$count" -gt 100 ]; then
echo "[output truncated to first 100 entries]"
fi
fi
rm -f $PIDFILE
echo "mover: finished"
echo "mover: finished" >&2
}
killtree() {
@@ -170,43 +133,18 @@ stop() {
# display usage and then exit
usage() {
echo "Usage: $0 start [-e <disk_name>]"
echo " $0 stop|status"
echo " <disk_name> must match pattern 'disk[0-9]*' and /mnt/<disk_name> must be a mountpoint"
echo "Usage: $0 start|stop|status"
exit 1
}
# validate disk name
validate_disk() {
if [[ ! "$1" =~ ^disk[0-9]+$ ]]; then
echo "Error: <disk_name> must match pattern 'disk[0-9]+$'"
usage
fi
if ! mountpoint --nofollow /mnt/$1 > /dev/null 2>&1; then
echo "Error: nothing mounted at /mnt/$1"
usage
fi
}
if [ "$#" -lt 1 ]; then
if [ "$#" -ne 1 ]; then
usage
exit 1
fi
case $1 in
start)
if [ -z "$2" ]; then
start
elif [ "$2" == "-e" ]; then
if [ -z "$3" ]; then
usage
else
validate_disk "$3"
empty "$3"
fi
else
usage
fi
start
;;
stop)
stop