Package Kit: Proper Installation for First Boot Packages

Do the final installation operations for all the packages in the
/system/packages directory when the OS is booted for the first time.

This will run their post-install scripts, create users, groups and generate
settings files (marked with a package version attribute).  Previously we just
ran all the shell scripts found in the /system/boot/post-install directory
(don't do that as much now).

Fixes bug #14382

This patch has simpler code flow in CommitTransactionHandler::_ApplyChanges
Tested on 32 and 64 bit systems.  Once it's official, need to remove the
open_ssh redundant post-install script that creates users etc. from HaikuPorts.
Now we can notice bugs like package version attributes on settings files aren't
fully working. :-)

Didn't remove special case for add_catalog_entry_attributes.sh since it
still does stuff that the build system doesn't do.  Might be able to add
that script as part of the Haiku.hpkg.  See change 3751 for removing it,
https://review.haiku-os.org/c/haiku/+/3751

Change-Id: I3807b78042fdb70e5a79eca2e2a45816ece0236f
Reviewed-on: https://review.haiku-os.org/c/haiku/+/2342
Reviewed-by: Alexander G. M. Smith <agmsmith@ncf.ca>
Reviewed-by: Niels Sascha Reedijk <niels.reedijk@gmail.com>
Reviewed-by: Adrien Destugues <pulkomandy@gmail.com>
This commit is contained in:
Alexander G. M. Smith 2021-02-02 17:27:06 -05:00 committed by Adrien Destugues
parent e459857d1c
commit 3376ed1a72
12 changed files with 261 additions and 56 deletions

32
data/system/boot/PostInstallScript Normal file → Executable file
View File

@ -1,6 +1,8 @@
#!/bin/sh
# Check for fresh install and run post install scripts.
# Check for fresh install and run oddball system post install stuff that the
# package_daemon doesn't handle. All Home and other post install scripts
# get run the old way here (probably won't be any, unless you reinstalled?).
title=$1
freshInstallIndicator=$2
@ -8,13 +10,29 @@ postInstallDir=$3
if [ -e $freshInstallIndicator ]; then
# execute scripts
for f in $postInstallDir/*.sh
do
if [ -f $f ]; then
echo "Running $title script $f ..." > /dev/dprintf
$f
if [ "$postInstallDir" == "/boot/system/boot/post-install" ]; then
# Special case for one script file that isn't in a package. Rest
# of the files in there will be run by package_daemon when
# doing first boot processing. Can be removed when Gerrit
# Change #3751 is done.
specialCaseFile="$postInstallDir/add_catalog_entry_attributes.sh"
if [ -e "$specialCaseFile" ]; then
echo "Running $title special case $specialCaseFile first boot processing." > /dev/dprintf
"$specialCaseFile"
else
echo "Skipping $title obsolete first boot processing, files:" > /dev/dprintf
ls "$postInstallDir" > /dev/dprintf
fi
done
else
for f in $postInstallDir/*.sh
do
if [ -f $f ]; then
echo "Running $title script $f ..." > /dev/dprintf
$f
fi
done
fi
sync
rm $freshInstallIndicator

View File

@ -179,7 +179,10 @@ The supported attributes are:
- ``post-install-scripts``: A list of paths of files included in the package,
which shall be executed on package activation. Each path must start with
"boot/post-install/". All the files in that directory are also run on first
boot after installing or copying the OS to a new disk.
boot after installing or copying the OS to a new disk. As an odd bonus,
they're also run when you boot the installer disc, and the installer copies
some of the resulting settings data to the new install too. So try to
handle being run twice.
- ``pre-uninstall-scripts``: A list of paths of files included in the package,
which shall be executed on package deactivation. For consistency, each path
should start with "boot/pre-uninstall/".

View File

@ -21,8 +21,8 @@ namespace BPrivate {
class BActivationTransaction : public BArchivable {
public:
BActivationTransaction();
BActivationTransaction(BMessage* archive,
status_t* _error = NULL);
BActivationTransaction(BMessage* archive,
status_t* _error = NULL);
virtual ~BActivationTransaction();
status_t SetTo(BPackageInstallationLocation location,
@ -52,6 +52,9 @@ public:
const BStringList& packages);
bool AddPackageToDeactivate(const BString& package);
bool FirstBootProcessing() const;
void SetFirstBootProcessing(bool processingIsOn);
virtual status_t Archive(BMessage* archive,
bool deep = true) const;
static BArchivable* Instantiate(BMessage* archive);
@ -66,6 +69,7 @@ private:
BString fTransactionDirectoryName;
BStringList fPackagesToActivate;
BStringList fPackagesToDeactivate;
bool fFirstBootProcessing;
};

View File

@ -47,6 +47,11 @@ enum {
// "deactivate": string[]
// file names of the packages to activate; must be in the
// transaction directory
// "first boot processing": bool
// if true then runs the package installation processing for
// specified (in the activate list) packages on the volume.
// Doesn't actually add the packages. Package_daemon preemptively
// deletes an empty transaction directory when done.
B_MESSAGE_COMMIT_TRANSACTION_REPLY = 'PKTR'
// "error": int32
// a BTransactionError describing how committing the transaction

View File

@ -312,6 +312,15 @@ WorkerThread::_LaunchFinishScript(BPath &path)
if (system(command.String()) != 0)
return B_ERROR;
// Ask for first boot processing of all the packages copied into the new
// installation, since by just copying them the normal package processing
// isn't done. package_daemon will detect the magic file and do it.
command.SetToFormat("echo 'First Boot written by Installer.' > "
"\"%s/system/packages/administrative/FirstBootProcessingNeeded\"",
path.Path());
if (system(command.String()) != 0)
return B_ERROR;
command.SetToFormat("rm -f \"%s/home/Desktop/Installer\"", path.Path());
return system(command.String());
}

View File

@ -24,7 +24,8 @@ BActivationTransaction::BActivationTransaction()
fChangeCount(0),
fTransactionDirectoryName(),
fPackagesToActivate(),
fPackagesToDeactivate()
fPackagesToDeactivate(),
fFirstBootProcessing(false)
{
}
@ -36,10 +37,16 @@ BActivationTransaction::BActivationTransaction(BMessage* archive,
fChangeCount(0),
fTransactionDirectoryName(),
fPackagesToActivate(),
fPackagesToDeactivate()
fPackagesToDeactivate(),
fFirstBootProcessing(false)
{
status_t error;
int32 location;
if (archive->FindBool("first boot processing", &fFirstBootProcessing)
!= B_OK)
fFirstBootProcessing = false; // Field is optional for compatibility.
if ((error = archive->FindInt32("location", &location)) == B_OK
&& (error = archive->FindInt64("change count", &fChangeCount)) == B_OK
&& (error = archive->FindString("transaction",
@ -91,6 +98,7 @@ BActivationTransaction::SetTo(BPackageInstallationLocation location,
fTransactionDirectoryName = directoryName;
fPackagesToActivate.MakeEmpty();
fPackagesToDeactivate.MakeEmpty();
fFirstBootProcessing = false;
return B_OK;
}
@ -183,6 +191,20 @@ BActivationTransaction::AddPackageToDeactivate(const BString& package)
}
bool
BActivationTransaction::FirstBootProcessing() const
{
return fFirstBootProcessing;
}
void
BActivationTransaction::SetFirstBootProcessing(bool processingIsOn)
{
fFirstBootProcessing = processingIsOn;
}
status_t
BActivationTransaction::Archive(BMessage* archive, bool deep) const
{
@ -197,7 +219,9 @@ BActivationTransaction::Archive(BMessage* archive, bool deep) const
|| (error = archive->AddStrings("activate", fPackagesToActivate))
!= B_OK
|| (error = archive->AddStrings("deactivate", fPackagesToDeactivate))
!= B_OK) {
!= B_OK
|| (error = archive->AddBool("first boot processing",
fFirstBootProcessing)) != B_OK) {
return error;
}

View File

@ -126,6 +126,7 @@ CommitTransactionHandler::CommitTransactionHandler(Volume* volume,
fOldStateDirectoryRef(),
fOldStateDirectoryName(),
fTransactionDirectoryRef(),
fFirstBootProcessing(false),
fWritableFilesDirectory(),
fAddedGroups(),
fAddedUsers(),
@ -182,6 +183,7 @@ void
CommitTransactionHandler::HandleRequest(BMessage* request)
{
status_t error;
BActivationTransaction transaction(request, &error);
if (error == B_OK)
error = transaction.InitCheck();
@ -203,6 +205,8 @@ CommitTransactionHandler::HandleRequest(
if (transaction.ChangeCount() != fVolume->ChangeCount())
throw Exception(B_TRANSACTION_CHANGE_COUNT_MISMATCH);
fFirstBootProcessing = transaction.FirstBootProcessing();
// collect the packages to deactivate
_GetPackagesToDeactivate(transaction);
@ -210,13 +214,30 @@ CommitTransactionHandler::HandleRequest(
_ReadPackagesToActivate(transaction);
// anything to do at all?
if (fPackagesToActivate.IsEmpty() && fPackagesToDeactivate.empty()) {
if (fPackagesToActivate.IsEmpty() && fPackagesToDeactivate.empty()) {
WARN("Bad package activation request: no packages to activate or"
" deactivate\n");
throw Exception(B_TRANSACTION_BAD_REQUEST);
}
_ApplyChanges();
// Clean up the unused empty transaction directory for first boot
// processing, since it's usually an internal to package_daemon
// operation and there is no external client to clean it up.
if (fFirstBootProcessing) {
RelativePath directoryPath(kAdminDirectoryName,
transaction.TransactionDirectoryName().String());
BDirectory transactionDir;
status_t error = _OpenPackagesSubDirectory(directoryPath, false,
transactionDir);
if (error == B_OK) {
BEntry transactionDirEntry;
error = transactionDir.GetEntry(&transactionDirEntry);
if (error == B_OK)
transactionDirEntry.Remove(); // Okay to fail when non-empty.
}
}
}
@ -336,22 +357,32 @@ CommitTransactionHandler::_ReadPackagesToActivate(
// read the packages
for (int32 i = 0; i < packagesToActivateCount; i++) {
BString packageName = packagesToActivate.StringAt(i);
// make sure it doesn't clash with an already existing package
// make sure it doesn't clash with an already existing package,
// except in first boot mode where it should always clash.
Package* package = fVolumeState->FindPackage(packageName);
if (package != NULL) {
if (fPackagesAlreadyAdded.find(package)
!= fPackagesAlreadyAdded.end()) {
if (!fPackagesToActivate.AddItem(package))
throw Exception(B_TRANSACTION_NO_MEMORY);
continue;
}
if (fPackagesToDeactivate.find(package)
== fPackagesToDeactivate.end()) {
throw Exception(B_TRANSACTION_PACKAGE_ALREADY_EXISTS)
if (fFirstBootProcessing) {
if (package == NULL) {
throw Exception(B_TRANSACTION_NO_SUCH_PACKAGE)
.SetPackageName(packageName);
}
if (!fPackagesToActivate.AddItem(package))
throw Exception(B_TRANSACTION_NO_MEMORY);
continue;
} else {
if (package != NULL) {
if (fPackagesAlreadyAdded.find(package)
!= fPackagesAlreadyAdded.end()) {
if (!fPackagesToActivate.AddItem(package))
throw Exception(B_TRANSACTION_NO_MEMORY);
continue;
}
if (fPackagesToDeactivate.find(package)
== fPackagesToDeactivate.end()) {
throw Exception(B_TRANSACTION_PACKAGE_ALREADY_EXISTS)
.SetPackageName(packageName);
}
}
}
// read the package
@ -382,27 +413,31 @@ CommitTransactionHandler::_ReadPackagesToActivate(
void
CommitTransactionHandler::_ApplyChanges()
{
// create an old state directory
_CreateOldStateDirectory();
if (!fFirstBootProcessing)
{
// create an old state directory
_CreateOldStateDirectory();
// move packages to deactivate to old state directory
_RemovePackagesToDeactivate();
// move packages to deactivate to old state directory
_RemovePackagesToDeactivate();
// move packages to activate to packages directory
_AddPackagesToActivate();
// move packages to activate to packages directory
_AddPackagesToActivate();
// run pre-uninstall scripts, before their packages vanish.
_RunPreUninstallScripts();
// run pre-uninstall scripts, before their packages vanish.
_RunPreUninstallScripts();
// activate/deactivate packages
_ChangePackageActivation(fAddedPackages, fRemovedPackages);
// activate/deactivate packages and create users, groups, settings files.
_ChangePackageActivation(fAddedPackages, fRemovedPackages);
} else // FirstBootProcessing, skip several steps and just do package setup.
_PrepareFirstBootPackages();
// run post-installation scripts
if (fVolumeStateIsActive) {
// run post-install scripts now that the new packages are visible in the
// package file system.
if (fVolumeStateIsActive || fFirstBootProcessing) {
_RunPostInstallScripts();
} else {
// need to reboot to finish installation so queue up scripts as
// symbolic links in a work directory, which will run later.
// Do post-install scripts later after a reboot, for Haiku OS packages.
_QueuePostInstallScripts();
}
@ -631,6 +666,31 @@ CommitTransactionHandler::_AddPackagesToActivate()
}
void
CommitTransactionHandler::_PrepareFirstBootPackages()
{
int32 count = fPackagesToActivate.CountItems();
BDirectory transactionDir(&fTransactionDirectoryRef);
BEntry transactionEntry;
BPath transactionPath;
if (transactionDir.InitCheck() == B_OK &&
transactionDir.GetEntry(&transactionEntry) == B_OK &&
transactionEntry.GetPath(&transactionPath) == B_OK) {
INFORM("Starting First Boot Processing for %d packages in %s.\n",
(int) count, transactionPath.Path());
}
for (int32 i = 0; i < count; i++) {
Package* package = fPackagesToActivate.ItemAt(i);
fAddedPackages.insert(package);
INFORM("Doing first boot processing #%d for package %s.\n",
(int) i, package->FileName().String());
_PreparePackageToActivate(package);
}
}
void
CommitTransactionHandler::_PreparePackageToActivate(Package* package)
{
@ -904,7 +964,8 @@ CommitTransactionHandler::_AddGlobalWritableFileRecurse(Package* package,
if (targetDirectory.GetStatFor(targetName, &targetStat) != B_OK) {
// target doesn't exist -- just copy
PRINT("Volume::CommitTransactionHandler::_AddGlobalWritableFile(): "
"couldn't get stat for writable file, copying...\n");
"couldn't get stat for writable file \"%s\", copying...\n",
targetName);
FSTransaction::CreateOperation copyOperation(&fFSTransaction,
FSUtils::Entry(targetDirectory, targetName));
status_t error = BCopyEngine(BCopyEngine::COPY_RECURSIVELY)
@ -947,7 +1008,8 @@ CommitTransactionHandler::_AddGlobalWritableFileRecurse(Package* package,
// Source and target entry types don't match or this is an entry
// we cannot handle. The user must handle this manually.
PRINT("Volume::CommitTransactionHandler::_AddGlobalWritableFile(): "
"writable file exists, but type doesn't match previous type\n");
"writable file \"%s\" exists, but type doesn't match previous "
"type\n", targetName);
_AddIssue(TransactionIssueBuilder(
BTransactionIssue::B_WRITABLE_FILE_TYPE_MISMATCH)
.SetPath1(FSUtils::Entry(targetDirectory, targetName))
@ -1003,7 +1065,9 @@ CommitTransactionHandler::_AddGlobalWritableFileRecurse(Package* package,
// Can't determine the original package. The user must handle this
// manually.
PRINT("Volume::CommitTransactionHandler::_AddGlobalWritableFile(): "
"failed to get SYS:PACKAGE attribute\n");
"failed to get SYS:PACKAGE attribute for \"%s\", can't tell if "
"file needs to be updated\n",
targetName);
if (updateType != B_WRITABLE_FILE_UPDATE_TYPE_KEEP_OLD) {
_AddIssue(TransactionIssueBuilder(
BTransactionIssue::B_WRITABLE_FILE_NO_PACKAGE_ATTRIBUTE)
@ -1015,7 +1079,8 @@ CommitTransactionHandler::_AddGlobalWritableFileRecurse(Package* package,
// If that's our package, we're happy.
if (originalPackage == package->RevisionedNameThrows()) {
PRINT("Volume::CommitTransactionHandler::_AddGlobalWritableFile(): "
"file tagged with same package version we're activating\n");
"file \"%s\" tagged with same package version we're activating\n",
targetName);
return;
}
@ -1073,8 +1138,8 @@ CommitTransactionHandler::_AddGlobalWritableFileRecurse(Package* package,
// handle this manually.
PRINT("Volume::CommitTransactionHandler::"
"_AddGlobalWritableFile(): "
"file comparison failed (%s) or files aren't equal\n",
strerror(error));
"file comparison \"%s\" failed (%s) or files aren't equal\n",
targetName, strerror(error));
if (updateType != B_WRITABLE_FILE_UPDATE_TYPE_KEEP_OLD) {
if (error != B_OK) {
_AddIssue(TransactionIssueBuilder(
@ -1108,8 +1173,8 @@ CommitTransactionHandler::_AddGlobalWritableFileRecurse(Package* package,
// handle this manually.
PRINT("Volume::CommitTransactionHandler::"
"_AddGlobalWritableFile(): "
"symlink comparison failed (%s) or symlinks aren't equal\n",
strerror(error));
"symlink comparison \"%s\" failed (%s) or symlinks aren't "
"equal\n", targetName, strerror(error));
if (updateType != B_WRITABLE_FILE_UPDATE_TYPE_KEEP_OLD) {
if (error != B_OK) {
_AddIssue(TransactionIssueBuilder(
@ -1189,7 +1254,7 @@ CommitTransactionHandler::_AddGlobalWritableFileRecurse(Package* package,
void
CommitTransactionHandler::_RevertAddPackagesToActivate()
{
if (fAddedPackages.empty())
if (fAddedPackages.empty() || fFirstBootProcessing)
return;
// open transaction directory
@ -1244,7 +1309,7 @@ CommitTransactionHandler::_RevertAddPackagesToActivate()
void
CommitTransactionHandler::_RevertRemovePackagesToDeactivate()
{
if (fRemovedPackages.empty())
if (fRemovedPackages.empty() || fFirstBootProcessing)
return;
// open packages directory
@ -1979,4 +2044,4 @@ CommitTransactionHandler::_AssertEntriesAreEqual(const BEntry& entry,
"Package file '%s' already exists in target folder "
"with equal contents\n", entry.Name());
return B_OK;
}
}

View File

@ -139,6 +139,7 @@ private:
const PackageSet& packagesToActivate,
const PackageSet& packagesToDeactivate);
// throws Exception
void _PrepareFirstBootPackages();
void _FillInActivationChangeItem(
PackageFSActivationChangeItem* item,
PackageFSActivationChangeType type,
@ -174,6 +175,7 @@ private:
node_ref fOldStateDirectoryRef;
BString fOldStateDirectoryName;
node_ref fTransactionDirectoryRef;
bool fFirstBootProcessing;
BDirectory fWritableFilesDirectory;
StringSet fAddedGroups;
StringSet fAddedUsers;

View File

@ -14,6 +14,8 @@ const char* const kAdminDirectoryName = PACKAGES_DIRECTORY_ADMIN_DIRECTORY;
const char* const kActivationFileName = PACKAGES_DIRECTORY_ACTIVATION_FILE;
const char* const kTemporaryActivationFileName
= PACKAGES_DIRECTORY_ACTIVATION_FILE ".tmp";
const char* const kFirstBootProcessingNeededFileName
= "FirstBootProcessingNeeded";
const char* const kWritableFilesDirectoryName = "writable-files";
const char* const kPackageFileAttribute = "SYS:PACKAGE";
const char* const kQueuedScriptsDirectoryName = "queued-scripts";

View File

@ -13,6 +13,7 @@ extern const char* const kPackageFileNameExtension;
extern const char* const kAdminDirectoryName;
extern const char* const kActivationFileName;
extern const char* const kTemporaryActivationFileName;
extern const char* const kFirstBootProcessingNeededFileName;
extern const char* const kWritableFilesDirectoryName;
extern const char* const kPackageFileAttribute;
extern const char* const kQueuedScriptsDirectoryName;

View File

@ -332,9 +332,75 @@ Volume::InitPackages(Listener* listener)
// create the admin directory, if it doesn't exist yet
BDirectory packagesDirectory;
bool createdAdminDirectory = false;
if (packagesDirectory.SetTo(&PackagesDirectoryRef()) == B_OK) {
if (!BEntry(&packagesDirectory, kAdminDirectoryName).Exists())
if (!BEntry(&packagesDirectory, kAdminDirectoryName).Exists()) {
packagesDirectory.CreateDirectory(kAdminDirectoryName, NULL);
createdAdminDirectory = true;
}
}
BDirectory adminDirectory(&packagesDirectory, kAdminDirectoryName);
error = adminDirectory.InitCheck();
if (error != B_OK)
RETURN_ERROR(error);
// First boot processing requested by a magic file left by the OS installer?
BEntry flagFileEntry(&adminDirectory, kFirstBootProcessingNeededFileName);
if (createdAdminDirectory || flagFileEntry.Exists()) {
INFORM("Volume::InitPackages Requesting delayed first boot processing "
"for packages dir %s.\n", BPath(&packagesDirectory).Path());
if (flagFileEntry.Exists())
flagFileEntry.Remove(); // Remove early on to avoid an error loop.
// Are there any packages needing processing? Don't want to create an
// empty transaction directory and then never have it cleaned up when
// the empty transaction gets rejected.
bool anyPackages = false;
for (PackageNodeRefHashTable::Iterator it =
fActiveState->ByNodeRefIterator(); it.HasNext();) {
Package* package = it.Next();
if (package->IsActive()) {
anyPackages = true;
break;
}
}
if (anyPackages) {
// Create first boot processing special transaction for current
// volume, which also creates an empty transaction directory.
BPackageInstallationLocation location = Location();
BDirectory transactionDirectory;
BActivationTransaction transaction;
error = CreateTransaction(location, transaction,
transactionDirectory);
if (error != B_OK)
RETURN_ERROR(error);
// Add all package files in currently active state to transaction.
for (PackageNodeRefHashTable::Iterator it =
fActiveState->ByNodeRefIterator(); it.HasNext();) {
Package* package = it.Next();
if (package->IsActive()) {
if (!transaction.AddPackageToActivate(
package->FileName().String()))
RETURN_ERROR(B_NO_MEMORY);
}
}
transaction.SetFirstBootProcessing(true);
// Queue up the transaction as a BMessage for processing a bit
// later, once the package daemon has finished initialising.
BMessage commitMessage(B_MESSAGE_COMMIT_TRANSACTION);
error = transaction.Archive(&commitMessage);
if (error != B_OK)
RETURN_ERROR(error);
BLooper *myLooper = Looper() ;
if (myLooper == NULL)
RETURN_ERROR(B_NOT_INITIALIZED);
error = myLooper->PostMessage(&commitMessage);
if (error != B_OK)
RETURN_ERROR(error);
}
}
return B_OK;
@ -1003,8 +1069,14 @@ Volume::_InitLatestStateFromActivatedPackages()
BFile file;
error = file.SetTo(&entryRef, B_READ_ONLY);
if (error != B_OK) {
INFORM("Failed to open packages activation file: %s\n",
strerror(error));
BEntry activationEntry(&entryRef);
BPath activationPath;
const char *activationFilePathName = "Unknown due to errors";
if (activationEntry.InitCheck() == B_OK &&
activationEntry.GetPath(&activationPath) == B_OK)
activationFilePathName = activationPath.Path();
INFORM("Failed to open packages activation file %s: %s\n",
activationFilePathName, strerror(error));
RETURN_ERROR(error);
}

View File

@ -5,7 +5,7 @@ resource app_flags B_EXCLUSIVE_LAUNCH | B_BACKGROUND_APP;
resource app_version {
major = 1,
middle = 0,
minor = 0,
minor = 1,
variety = B_APPV_ALPHA,
internal = 0,