diff --git a/emhttp/plugins/dynamix.gui.search/sheets/gui_search.css b/emhttp/plugins/dynamix.gui.search/sheets/gui_search.css old mode 100644 new mode 100755 index aaef0d82e..8788ebed8 --- a/emhttp/plugins/dynamix.gui.search/sheets/gui_search.css +++ b/emhttp/plugins/dynamix.gui.search/sheets/gui_search.css @@ -58,6 +58,7 @@ #guiSearchBoxSpan { margin: 0; padding: 0; + height: 18px !important; } #guiSearchBox { position: relative; diff --git a/emhttp/plugins/dynamix.plugin.manager/post-hooks/post_plugin_checks b/emhttp/plugins/dynamix.plugin.manager/post-hooks/post_plugin_checks index f84d646d6..8f10eaf56 100755 --- a/emhttp/plugins/dynamix.plugin.manager/post-hooks/post_plugin_checks +++ b/emhttp/plugins/dynamix.plugin.manager/post-hooks/post_plugin_checks @@ -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"); diff --git a/emhttp/plugins/dynamix.plugin.manager/scripts/plugin b/emhttp/plugins/dynamix.plugin.manager/scripts/plugin index 8d601aced..48a3853c5 100755 --- a/emhttp/plugins/dynamix.plugin.manager/scripts/plugin +++ b/emhttp/plugins/dynamix.plugin.manager/scripts/plugin @@ -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); diff --git a/emhttp/plugins/dynamix.vm.manager/VMMachines.page b/emhttp/plugins/dynamix.vm.manager/VMMachines.page index b6da578d3..69e450aa7 100755 --- a/emhttp/plugins/dynamix.vm.manager/VMMachines.page +++ b/emhttp/plugins/dynamix.vm.manager/VMMachines.page @@ -3,6 +3,7 @@ Title="Virtual Machines" Tag="columns" Cond="is_file('/var/run/libvirt/libvirtd.pid')" Markdown="false" +Focus="tableHeaderResize" --- - 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 - 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(); - }); - - + }, 150); + }); + }); } + +// 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() { + + 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'); + +} + $(function() { diff --git a/emhttp/plugins/dynamix.vm.manager/novnc/defaults.json b/emhttp/plugins/dynamix.vm.manager/novnc/defaults.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/emhttp/plugins/dynamix.vm.manager/novnc/defaults.json @@ -0,0 +1 @@ +{} diff --git a/emhttp/plugins/dynamix.vm.manager/novnc/mandatory.json b/emhttp/plugins/dynamix.vm.manager/novnc/mandatory.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/emhttp/plugins/dynamix.vm.manager/novnc/mandatory.json @@ -0,0 +1 @@ +{} diff --git a/emhttp/plugins/dynamix.vm.manager/templates/Custom.form.php b/emhttp/plugins/dynamix.vm.manager/templates/Custom.form.php old mode 100644 new mode 100755 index 7ad13979d..584f58ade --- a/emhttp/plugins/dynamix.vm.manager/templates/Custom.form.php +++ b/emhttp/plugins/dynamix.vm.manager/templates/Custom.form.php @@ -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) { ?> _(VM Console Port)_: - + _(VM Console WS Port)_: - + @@ -2124,6 +2124,18 @@ foreach ($arrConfig['evdev'] as $i => $arrEvdev) { var storageType = ""; var storageLoc = ""; +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 address of wlan0 var mac = $('input[name="nic['+index+'][mac]"'); @@ -2635,6 +2647,13 @@ $(function() { }); + $("#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) { diff --git a/emhttp/plugins/dynamix/ArrayOperation.page b/emhttp/plugins/dynamix/ArrayOperation.page old mode 100644 new mode 100755 index 74381e295..4f452b11f --- a/emhttp/plugins/dynamix/ArrayOperation.page +++ b/emhttp/plugins/dynamix/ArrayOperation.page @@ -348,8 +348,40 @@ function shutdown_now(form, cmd) { $(form).append(''); 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')) { + $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.)_"; + + text += " _(If you use Tailscale or Unraid Connect Remote Access to access the webGUI or services of the server, this access will be lost.)_"; + + text += "\n\n_(You are currently accessing the webGUI of the server via Tailscale.)_"; + + + text += "\n\n_(You are currently accessing the webGUI of the server via Unraid Connect Remote Access.)_"; + + + } else { + var text = "_(This will reboot the server.)_"; + } + break; + case 'shutdown': + var text = "_(This will shutdown the server.)_"; + break; } swal({ title:"_(Proceed)_?", diff --git a/emhttp/plugins/dynamix/DashStats.page b/emhttp/plugins/dynamix/DashStats.page index 5522666ae..60fbc8f96 100755 --- a/emhttp/plugins/dynamix/DashStats.page +++ b/emhttp/plugins/dynamix/DashStats.page @@ -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 - + _(Total)_ _(Power)_: N/A + + + _(Temperature)_: N/A + + _(Load)_: 0% @@ -398,22 +403,29 @@ switch ($themeHelper->getThemeName()) { // $themeHelper set in DefaultPageLayout - + "; - 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 " "._("Physical")." CPU $cpu_index "._("Power").": N/A "; + if (count($cpus)>1) echo " "._("Temperature").": N/A"; + echo ""; + } + foreach ($package as $pair) { + [$cpu1, $cpu2] = my_preg_split('/[,-]/',$pair); + echo ""; + if ($is_intel_cpu && count($core_types) > 0) + $core_type = "({$core_types[$cpu1]})"; + else + $core_type = ""; - if ($cpu2) - echo "CPU $cpu1 $core_type - HT $cpu2 0%
0%
"; - else - echo "CPU $cpu1 $core_type0%
"; - echo ""; - } + if ($cpu2) + echo "CPU $cpu1 $core_type - HT $cpu2 0%
0%
"; + else + echo "CPU $cpu1 $core_type0%
"; + echo ""; + } + } ?> @@ -1441,6 +1453,7 @@ var startup = true; var stopgap = ''; var recall = null; var recover = null; +var tempunit=""; // 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 = ` _(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)+` °`+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() { diff --git a/emhttp/plugins/dynamix/DeviceInfo.page b/emhttp/plugins/dynamix/DeviceInfo.page index 88df03ea2..e07dd554b 100755 --- a/emhttp/plugins/dynamix/DeviceInfo.page +++ b/emhttp/plugins/dynamix/DeviceInfo.page @@ -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)_ (%): - +
_(Pool Device Status)_ diff --git a/emhttp/plugins/dynamix/MoverSettings.page b/emhttp/plugins/dynamix/MoverSettings.page index 4ca5a060e..e6576111a 100755 --- a/emhttp/plugins/dynamix/MoverSettings.page +++ b/emhttp/plugins/dynamix/MoverSettings.page @@ -15,6 +15,8 @@ Tag="calendar-check-o" */ ?> "._('No Cache device present')."!

"; @@ -23,11 +25,13 @@ if (!$pool_devices) { echo "

"._('User shares not enabled')."!

"; $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); }); +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();} -}
@@ -92,7 +102,7 @@ _(Day of the month)_: _(Time of the day)_: : - style="display:none"> @@ -102,7 +112,7 @@ _(Time of the day)_:   _(HH:MM)_ - style="display:none"> diff --git a/emhttp/plugins/dynamix/NotificationAgents.page b/emhttp/plugins/dynamix/NotificationAgents.page index af6d65bf3..b6ecebc35 100644 --- a/emhttp/plugins/dynamix/NotificationAgents.page +++ b/emhttp/plugins/dynamix/NotificationAgents.page @@ -1,9 +1,10 @@ Menu="Notifications:3" Title="Notification Agents" Tag="rss-square" +Focus="initDropdown" --- - -$('#tab3').bind({click:function(){initDropdown();}}); -$(function(){if ($('#tab3').is(':checked')) initDropdown();}); - + + $(function(){initDropdown();}); - + +$(function(){ + if ( $('#tab3').attr('checked') ) initDropdown(); +}); + if (!String.prototype.format) { String.prototype.format = function() { diff --git a/emhttp/plugins/dynamix/SecurityNFS.page b/emhttp/plugins/dynamix/SecurityNFS.page index da4a9cf20..ce7d4f478 100755 --- a/emhttp/plugins/dynamix/SecurityNFS.page +++ b/emhttp/plugins/dynamix/SecurityNFS.page @@ -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 = .replace(/\n/g, ' '); diff --git a/emhttp/plugins/dynamix/agents/Join.xml b/emhttp/plugins/dynamix/agents/Join.xml new file mode 100644 index 000000000..19872f120 --- /dev/null +++ b/emhttp/plugins/dynamix/agents/Join.xml @@ -0,0 +1,25 @@ + + Join + + API_KEY + TITLE + MESSAGE + + + diff --git a/emhttp/plugins/dynamix/include/DefaultPageLayout/Header.php b/emhttp/plugins/dynamix/include/DefaultPageLayout/Header.php old mode 100644 new mode 100755 index ea6e88031..579dd637d --- a/emhttp/plugins/dynamix/include/DefaultPageLayout/Header.php +++ b/emhttp/plugins/dynamix/include/DefaultPageLayout/Header.php @@ -1,5 +1,7 @@ diff --git a/emhttp/plugins/dynamix/include/DefaultPageLayout/Navigation/Main.php b/emhttp/plugins/dynamix/include/DefaultPageLayout/Navigation/Main.php old mode 100644 new mode 100755 index 3d4b5a0e0..40bd5e99d --- a/emhttp/plugins/dynamix/include/DefaultPageLayout/Navigation/Main.php +++ b/emhttp/plugins/dynamix/include/DefaultPageLayout/Navigation/Main.php @@ -37,10 +37,12 @@
- + isSidebarTheme()): ?> - + isSidebarTheme()): ?> + + diff --git a/emhttp/plugins/dynamix/include/Helpers.php b/emhttp/plugins/dynamix/include/Helpers.php index 8b4bc99c7..16fcdb874 100644 --- a/emhttp/plugins/dynamix/include/Helpers.php +++ b/emhttp/plugins/dynamix/include/Helpers.php @@ -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"; diff --git a/emhttp/plugins/dynamix/include/cpulist.php b/emhttp/plugins/dynamix/include/cpulist.php new file mode 100644 index 000000000..8d1db3573 --- /dev/null +++ b/emhttp/plugins/dynamix/include/cpulist.php @@ -0,0 +1,22 @@ + (int)explode($separator, $s)[0], $list); + array_multisort($keys, SORT_ASC, SORT_NUMERIC, $list); + } + unset($list); + + return $packages; +} diff --git a/emhttp/plugins/dynamix/scripts/diagnostics b/emhttp/plugins/dynamix/scripts/diagnostics index e6d3be1f8..859c37b89 100755 --- a/emhttp/plugins/dynamix/scripts/diagnostics +++ b/emhttp/plugins/dynamix/scripts/diagnostics @@ -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")); diff --git a/emhttp/plugins/dynamix/scripts/notify b/emhttp/plugins/dynamix/scripts/notify index 8a5adc246..1c4db802b 100755 --- a/emhttp/plugins/dynamix/scripts/notify +++ b/emhttp/plugins/dynamix/scripts/notify @@ -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','
',$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('
','. ',$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','
',$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('
','. ',$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++; } diff --git a/emhttp/plugins/dynamix/styles/default-base.css b/emhttp/plugins/dynamix/styles/default-base.css index 164202fdc..1f358713c 100755 --- a/emhttp/plugins/dynamix/styles/default-base.css +++ b/emhttp/plugins/dynamix/styles/default-base.css @@ -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; } diff --git a/etc/rc.d/rc.M b/etc/rc.d/rc.M index 714bdf993..c875bf3ad 100755 --- a/etc/rc.d/rc.M +++ b/etc/rc.d/rc.M @@ -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). diff --git a/etc/rc.d/rc.S.cont b/etc/rc.d/rc.S.cont index f5a4f954f..487d658bf 100755 --- a/etc/rc.d/rc.S.cont +++ b/etc/rc.d/rc.S.cont @@ -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 /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 diff --git a/etc/rc.d/rc.avahidaemon b/etc/rc.d/rc.avahidaemon index acdd434f1..33195164f 100755 --- a/etc/rc.d/rc.avahidaemon +++ b/etc/rc.d/rc.avahidaemon @@ -31,7 +31,7 @@ CALLER="avahi" AVAHI="/usr/sbin/avahi-daemon" CONF="/etc/avahi/avahi-daemon.conf" HOSTS="/etc/hosts" -NAME=$(/dev/null + if avahid_running; then + add_local_to_hosts + $AVAHI --reload 2>/dev/null + fi } avahid_update(){ diff --git a/etc/rc.d/rc.docker b/etc/rc.d/rc.docker index d5ba3bf29..e9ac334d9 100755 --- a/etc/rc.d/rc.docker +++ b/etc/rc.d/rc.docker @@ -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." diff --git a/etc/rc.d/rc.local b/etc/rc.d/rc.local index d59ea8fca..5fbe4263d 100755 --- a/etc/rc.d/rc.local +++ b/etc/rc.d/rc.local @@ -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 diff --git a/etc/rc.d/rc.samba b/etc/rc.d/rc.samba index 8d30144f8..a098b68c6 100755 --- a/etc/rc.d/rc.samba +++ b/etc/rc.d/rc.samba @@ -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 diff --git a/sbin/mover b/sbin/mover index 62a2afb2c..25107d999 100755 --- a/sbin/mover +++ b/sbin/mover @@ -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 ]" - echo " $0 stop|status" - echo " must match pattern 'disk[0-9]*' and /mnt/ 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: 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