Skip to content

Commit

Permalink
User installed OS packages part 1
Browse files Browse the repository at this point in the history
  • Loading branch information
darylc committed Jan 1, 2025
1 parent cc72fae commit 25fa492
Show file tree
Hide file tree
Showing 4 changed files with 372 additions and 1 deletion.
76 changes: 76 additions & 0 deletions www/api/controllers/system.php
Original file line number Diff line number Diff line change
Expand Up @@ -399,3 +399,79 @@ function finalizeStatusJson($obj)

return $obj;
}

// GET /api/system/GetOSPackages
/**
* Get a list of all available packages on the system.
*
* @return array List of package names.
*/
function GetOSPackages() {
$packages = [];
$cmd = 'apt list --all-versions 2>&1'; // Fetch all package names and versions
$handle = popen($cmd, 'r'); // Open a process for reading the output

if ($handle) {
while (($line = fgets($handle)) !== false) {
// Extract the package name before the slash
if (preg_match('/^([^\s\/]+)\//', $line, $matches)) {
$packages[] = $matches[1];
}
}
pclose($handle); // Close the process
} else {
error_log("Error: Unable to fetch package list.");
}

return json_encode($packages);
}
/**
* Get information about a specific package.
*
* This function retrieves the description, dependencies, and installation status for a given package.
*
* @param string $packageName The name of the package.
* @return array An associative array containing 'Description', 'Depends', and 'Installed'.
*/
function GetOSPackageInfo() {
$packageName = params('packageName');

// Fetch package information using apt-cache show
$output = shell_exec("apt-cache show " . escapeshellarg($packageName) . " 2>&1");

if (!$output) {
return ['error' => "Package '$packageName' not found or no information available."];
}

// Check installation status using dpkg-query
$installStatus = shell_exec("/usr/bin/dpkg-query -W -f='\${Status}\n' " . escapeshellarg($packageName) . " 2>&1");
error_log("Raw dpkg-query output for $packageName: |" . $installStatus . "|");

// Trim and validate output
$trimmedStatus = trim($installStatus);
error_log("Trimmed dpkg-query output for $packageName: |" . $trimmedStatus . "|");

$isInstalled = ($trimmedStatus === 'install ok installed') ? 'Yes' : 'No';

// Parse apt-cache output
$lines = explode("\n", $output);
$description = '';
$depends = '';

foreach ($lines as $line) {
if (strpos($line, 'Description:') === 0) {
$description = trim(substr($line, strlen('Description:')));
} elseif (strpos($line, 'Depends:') === 0) {
$depends = trim(substr($line, strlen('Depends:')));
}
}

return json_encode([
'Description' => $description,
'Depends' => $depends,
'Installed' => $isInstalled
]);
}



2 changes: 2 additions & 0 deletions www/api/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@
dispatch_post('/system/volume', 'SystemSetAudio');
dispatch_post('/system/proxies', 'PostProxies');
dispatch_get('/system/proxies', 'GetProxies');
dispatch_get('/system/packages', 'GetOSpackages');
dispatch_get('/system/packages/info/:packageName', 'GetOSpackageInfo');

dispatch_get('/testmode', 'testMode_Get');
dispatch_post('/testmode', 'testMode_Set');
Expand Down
5 changes: 4 additions & 1 deletion www/menu.inc
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,9 @@ function list_plugin_entries($menu)
Browser</a>
<a class="dropdown-item" href="plugins.php"><i class="fas fa-puzzle-piece"></i> Plugin
Manager</a>
<? if ($uiLevel >= 2) { ?>
<a class="dropdown-item" href="packages.php"><i class="fas fa-box"></i> Packages </a>
<? } ?>
<?php list_plugin_entries("content"); ?>
</div>
</li>
Expand Down Expand Up @@ -445,4 +448,4 @@ function list_plugin_entries($menu)
</nav>


</div>
</div>
290 changes: 290 additions & 0 deletions www/packages.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,290 @@
<?php
if (isset($_REQUEST['action'])) {
$skipJSsettings = 1;
}
require_once('config.php');
require_once('common.php');
DisableOutputBuffering();

$userPackagesFile = $settings['configDirectory'] . '/userpackages.json';
$userPackages = [];
if (file_exists($userPackagesFile)) {
$userPackages = json_decode(file_get_contents($userPackagesFile), true);
if (!is_array($userPackages)) {
$userPackages = [];
}
}

// Handle backend actions
$action = $_POST['action'] ?? $_GET['action'] ?? null;
$packageName = $_POST['package'] ?? $_GET['package'] ?? null;

if ($action) {
if ($action === 'install' && !empty($packageName)) {
$packageName = escapeshellarg($packageName);
header('Content-Type: text/plain');

$process = popen("sudo apt-get install -y $packageName 2>&1", 'r');
if (is_resource($process)) {
while (!feof($process)) {
echo fread($process, 1024);
flush();
}
pclose($process);
}

// Add package to user-installed packages if not already added
if (!in_array(trim($packageName, "'"), $userPackages)) {
$userPackages[] = trim($packageName, "'");
file_put_contents($userPackagesFile, json_encode($userPackages, JSON_PRETTY_PRINT));
}

exit;
}

if ($action === 'uninstall' && !empty($packageName)) {
$packageName = escapeshellarg($packageName);
header('Content-Type: text/plain');

$process = popen("sudo apt-get remove -y $packageName 2>&1", 'r');
if (is_resource($process)) {
while (!feof($process)) {
echo fread($process, 1024);
flush();
}
pclose($process);
}

// Remove package from user-installed packages
$userPackages = array_filter($userPackages, function($pkg) use ($packageName) {
return $pkg !== trim($packageName, "'");
});
file_put_contents($userPackagesFile, json_encode($userPackages, JSON_PRETTY_PRINT));

exit;
}
}
include 'common/menuHead.inc';
writeFPPVersionJavascriptFunctions();
?>
<style>
.taller-modal .modal-dialog {
max-height: 90%;
height: 90%;
overflow-y: auto;
}

.taller-modal .modal-body {
max-height: calc(100% - 120px);
overflow-y: auto;
}
</style>
<script>
var systemPackages = [];
var userInstalledPackages = <?php echo json_encode($userPackages); ?>;
var selectedPackageName = "";

function ShowLoadingIndicator() {
$('#loadingIndicator').show();
$('#packageInputContainer').hide();
}

function HideLoadingIndicator() {
$('#loadingIndicator').hide();
$('#packageInputContainer').show();
}

function GetSystemPackages() {
ShowLoadingIndicator();
$.ajax({
url: '/api/system/packages',
type: 'GET',
dataType: 'json',
success: function (data) {
if (!data || !Array.isArray(data)) {
console.error('Invalid data received from server.', data);
alert('Error: Unable to retrieve package list.');
return;
}
console.log('Raw Data Received:', data);
systemPackages = data;
console.log('Parsed Packages:', systemPackages);
InitializeAutocomplete();
HideLoadingIndicator();
},
error: function () {
alert('Error, failed to get system packages list.');
HideLoadingIndicator();
}
});
}

function GetPackageInfo(packageName) {
selectedPackageName = packageName;
$.ajax({
url: `/api/system/packages/info/${encodeURIComponent(packageName)}`,
type: 'GET',
dataType: 'json',
success: function (data) {
if (data.error) {
alert(`Error: ${data.error}`);
return;
}
const description = data.Description || 'No description available.';
const dependencies = data.Depends
? data.Depends.replace(/\([^)]*\)/g, '').trim()
: 'No dependencies.';
const installed = data.Installed === "Yes" ? "(Already Installed)" : "";
$('#packageInfo').html(`
<strong>Selected Package:</strong> ${packageName} ${installed}<br>
${data.Installed !== "Yes" ?`
<strong>Description:</strong> ${description}<br>
<strong>Will also install these packages:</strong> ${dependencies}<br>
<div class="buttons btn-lg btn-rounded btn-outline-success mt-2" onClick='InstallPackage();'>
<i class="fas fa-download"></i> Install Package
</div>` : ""}
`);
},
error: function () {
alert('Error, failed to fetch package information.');
}
});
}

function InitializeAutocomplete() {
if (!systemPackages.length) {
console.warn('System packages list is empty.');
return;
}

$("#packageInput").autocomplete({
source: systemPackages,
select: function (event, ui) {
const selectedPackage = ui.item.value;
$(this).val(selectedPackage);
return false;
}
});
}

function InstallPackage() {
if (!selectedPackageName) {
alert('Please select a package and retrieve its info before installing.');
return;
}

const url = `packages.php?action=install&package=${encodeURIComponent(selectedPackageName)}`;
$('#packageProgressPopupCloseButton').text('Please Wait').prop("disabled", true);
DisplayProgressDialog("packageProgressPopup", `Installing Package: ${selectedPackageName}`);
StreamURL(
url,
'packageProgressPopupText',
'EnableCloseButtonAfterOperation',
'EnableCloseButtonAfterOperation'
);
}

function UninstallPackage(packageName) {
const url = `packages.php?action=uninstall&package=${encodeURIComponent(packageName)}`;
$('#packageProgressPopupCloseButton').text('Please Wait').prop("disabled", true);
DisplayProgressDialog("packageProgressPopup", `Uninstalling Package: ${packageName}`);
StreamURL(
url,
'packageProgressPopupText',
'EnableCloseButtonAfterOperation',
'EnableCloseButtonAfterOperation'
);
}

function EnableCloseButtonAfterOperation(id) {
$('#packageProgressPopupCloseButton')
.text('Close')
.prop("disabled", false)
.on('click', function () {
$('#packageInput').val(''); // Clear the input field
location.reload(); // Refresh the page when the close button is clicked
});
}

$(document).ready(function () {
GetSystemPackages();

const userPackagesList = userInstalledPackages.map(pkg => `<li>${pkg} <button class='btn btn-sm btn-outline-danger' onClick='UninstallPackage("${pkg}");'>Uninstall</button></li>`).join('');
$('#userPackagesList').html(userPackagesList || '<p>No user-installed packages found.</p>');

$('#packageInput').on('input', function () {
const packageName = $(this).val().trim();
if (packageName) {
$('#packageStatus').text('');
}
});
});
</script>
<title>Package Manager</title>
</head>

<body>
<div id="bodyWrapper">
<?php
$activeParentMenuItem = 'content';
include 'menu.inc'; ?>
<div class="mainContainer">
<h1 class="title">Package Manager</h1>
<div class="pageContent">
<div id="packages" class="settings">


<h2>Please Note:</h2>
Installing additional packages can break your FPP installation requiring complete reinstallation of FPP. Continue at your own risk.
<p>
<h2>Installed User Packages</h2>
<ul id="userPackagesList"></ul>

<div id="loadingIndicator" style="display: none; text-align: center;">
<p>Loading package list, please wait...</p>
</div>

<div id="packageInputContainer" style="display: none;">
<div class="row">
<div class="col">
<input type="text" id="packageInput" class="form-control form-control-lg form-control-rounded has-shadow" placeholder="Enter package name" />
</div>
<div class="col-auto">
<div class="buttons btn-lg btn-rounded btn-outline-info" onClick='GetPackageInfo($("#packageInput").val().trim());'>
<i class="fas fa-info-circle"></i> Get Info
</div>
</div>
</div>
</div>

<div class='packageDiv'>
<div id="packageInfo" class="mt-3 text-muted"></div>
<div id="overlay"></div>
</div>
</div>

<div id="packageProgressPopup" class="modal taller-modal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Installing Package</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<pre id="packageProgressPopupText" style="white-space: pre-wrap;"></pre>
</div>
<div class="modal-footer">
<button id="packageProgressPopupCloseButton" type="button" class="btn btn-secondary" data-bs-dismiss="modal" disabled>Close</button>
</div>
</div>
</div>
</div>

</div>
</div>
<?php include 'common/footer.inc'; ?>
</div>
</body>

</html>

0 comments on commit 25fa492

Please sign in to comment.