Files
mgmt/engine/resources/packagekit/packagekit.go
James Shubin d30ff6cfae legal: Remove year
Instead of constantly making these updates, let's just remove the year
since things are stored in git anyways, and this is not an actual modern
legal risk anymore.
2025-01-26 16:24:51 -05:00

977 lines
31 KiB
Go

// Mgmt
// Copyright (C) James Shubin and the project contributors
// Written by James Shubin <james@shubin.ca> and the project contributors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//
// Additional permission under GNU GPL version 3 section 7
//
// If you modify this program, or any covered work, by linking or combining it
// with embedded mcl code and modules (and that the embedded mcl code and
// modules which link with this program, contain a copy of their source code in
// the authoritative form) containing parts covered by the terms of any other
// license, the licensors of this program grant you additional permission to
// convey the resulting work. Furthermore, the licensors of this program grant
// the original author, James Shubin, additional permission to update this
// additional permission if he deems it necessary to achieve the goals of this
// additional permission.
// Package packagekit provides an interface to interact with packagekit.
// See: https://www.freedesktop.org/software/PackageKit/gtk-doc/index.html for
// more information.
package packagekit
import (
"fmt"
"runtime"
"strings"
engineUtil "github.com/purpleidea/mgmt/engine/util"
"github.com/purpleidea/mgmt/util"
archUtil "github.com/purpleidea/mgmt/util/arch"
"github.com/purpleidea/mgmt/util/errwrap"
"github.com/godbus/dbus/v5"
)
// global tweaks of verbosity and code path
const (
Paranoid = false // enable if you see any ghosts
)
// constants which might need to be tweaked or which contain special dbus
// strings.
const (
// FIXME: if PkBufferSize is too low, install seems to drop signals
PkBufferSize = 1000
// TODO: the PkSignalTimeout value might be too low
PkSignalPackageTimeout = 60 // 60 seconds, arbitrary
PkSignalDestroyTimeout = 15 // 15 seconds, arbitrary
PkPath = "/org/freedesktop/PackageKit"
PkIface = "org.freedesktop.PackageKit"
PkIfaceTransaction = PkIface + ".Transaction"
)
// type enum_filter uint64
// https://github.com/hughsie/PackageKit/blob/master/lib/packagekit-glib2/pk-enum.c
const ( //static const PkEnumMatch enum_filter[]
PkFilterEnumUnknown uint64 = 1 << iota // "unknown"
PkFilterEnumNone // "none"
PkFilterEnumInstalled // "installed"
PkFilterEnumNotInstalled // "~installed"
PkFilterEnumDevelopment // "devel"
PkFilterEnumNotDevelopment // "~devel"
PkFilterEnumGui // "gui"
PkFilterEnumNotGui // "~gui"
PkFilterEnumFree // "free"
PkFilterEnumNotFree // "~free"
PkFilterEnumVisible // "visible"
PkFilterEnumNotVisible // "~visible"
PkFilterEnumSupported // "supported"
PkFilterEnumNotSupported // "~supported"
PkFilterEnumBasename // "basename"
PkFilterEnumNotBasename // "~basename"
PkFilterEnumNewest // "newest"
PkFilterEnumNotNewest // "~newest"
PkFilterEnumArch // "arch"
PkFilterEnumNotArch // "~arch"
PkFilterEnumSource // "source"
PkFilterEnumNotSource // "~source"
PkFilterEnumCollections // "collections"
PkFilterEnumNotCollections // "~collections"
PkFilterEnumApplication // "application"
PkFilterEnumNotApplication // "~application"
PkFilterEnumDownloaded // "downloaded"
PkFilterEnumNotDownloaded // "~downloaded"
)
// constants from packagekit c library.
const ( //static const PkEnumMatch enum_transaction_flag[]
PkTransactionFlagEnumNone uint64 = 1 << iota // "none"
PkTransactionFlagEnumOnlyTrusted // "only-trusted"
PkTransactionFlagEnumSimulate // "simulate"
PkTransactionFlagEnumOnlyDownload // "only-download"
PkTransactionFlagEnumAllowReinstall // "allow-reinstall"
PkTransactionFlagEnumJustReinstall // "just-reinstall"
PkTransactionFlagEnumAllowDowngrade // "allow-downgrade"
)
// constants from packagekit c library.
const ( //typedef enum
PkInfoEnumUnknown uint64 = 1 << iota
PkInfoEnumInstalled
PkInfoEnumAvailable
PkInfoEnumLow
PkInfoEnumEnhancement
PkInfoEnumNormal
PkInfoEnumBugfix
PkInfoEnumImportant
PkInfoEnumSecurity
PkInfoEnumBlocked
PkInfoEnumDownloading
PkInfoEnumUpdating
PkInfoEnumInstalling
PkInfoEnumRemoving
PkInfoEnumCleanup
PkInfoEnumObsoleting
PkInfoEnumCollectionInstalled
PkInfoEnumCollectionAvailable
PkInfoEnumFinished
PkInfoEnumReinstalling
PkInfoEnumDowngrading
PkInfoEnumPreparing
PkInfoEnumDecompressing
PkInfoEnumUntrusted
PkInfoEnumTrusted
PkInfoEnumUnavailable
PkInfoEnumLast
)
// Conn is a wrapper struct so we can pass bus connection around in the struct.
type Conn struct {
conn *dbus.Conn
Debug bool
Logf func(format string, v ...interface{})
}
// PkPackageIDActionData is a struct that is returned by PackagesToPackageIDs in
// the map values.
type PkPackageIDActionData struct {
Found bool
Installed bool
Version string
PackageID string
Newest bool
}
// NewBus returns a new bus connection.
func NewBus() *Conn {
// if we share the bus with others, we will get each others messages!!
bus, err := util.SystemBusPrivateUsable() // don't share the bus connection!
if err != nil {
return nil
}
return &Conn{
conn: bus,
}
}
// GetBus gets the dbus connection object.
func (obj *Conn) GetBus() *dbus.Conn {
return obj.conn
}
// Close closes the dbus connection object.
func (obj *Conn) Close() error {
return obj.conn.Close()
}
// matchSignal is an internal helper to add signal matches to the bus. It should
// only be called once.
func (obj *Conn) matchSignal(ch chan *dbus.Signal, path dbus.ObjectPath, iface string, signals []string) (func() error, error) {
if obj.Debug {
obj.Logf("matchSignal(%v, %v, %s, %v)", ch, path, iface, signals)
}
// eg: gdbus monitor --system --dest org.freedesktop.PackageKit --object-path /org/freedesktop/PackageKit | grep <signal>
bus := obj.GetBus().BusObject()
var argsList []string
// cleanup function should be called when done or when AddMatch errors
removeSignals := func() error {
var errList error
for i := len(argsList) - 1; i >= 0; i-- { // last in first out
call := bus.Call(engineUtil.DBusRemoveMatch, 0, argsList[i])
errList = errwrap.Append(errList, call.Err)
}
return errList
}
// TODO: if we make this call many times, we seem to receive signals
// that many times... Maybe this should be an object singleton?
var call *dbus.Call
pathStr := fmt.Sprintf("%s", path)
if len(signals) == 0 {
args := fmt.Sprintf("type='signal', path='%s', interface='%s'", pathStr, iface)
argsList = append(argsList, args)
call = bus.Call(engineUtil.DBusAddMatch, 0, args)
} else {
for _, signal := range signals {
args := fmt.Sprintf("type='signal', path='%s', interface='%s', member='%s'", pathStr, iface, signal)
argsList = append(argsList, args)
if call = bus.Call(engineUtil.DBusAddMatch, 0, args); call.Err != nil {
break // fail if any one fails
}
}
}
if call.Err != nil {
defer removeSignals() // ignore the error
return nil, call.Err
}
// The caller has to make sure that ch is sufficiently buffered; if a
// message arrives when a write to c is not possible, it is discarded!
// This can be disastrous if we're waiting for a "Finished" signal!
obj.GetBus().Signal(ch)
return removeSignals, nil
}
// WatchChanges gets a signal anytime an event happens.
func (obj *Conn) WatchChanges() (chan *dbus.Signal, error) {
ch := make(chan *dbus.Signal, PkBufferSize)
// NOTE: the TransactionListChanged signal fires much more frequently,
// but with much less specificity. If we're missing events, report the
// issue upstream! The UpdatesChanged signal is what hughsie suggested
var signal = "UpdatesChanged"
removeSignals, err := obj.matchSignal(ch, PkPath, PkIface, []string{signal})
if err != nil {
return nil, err
}
defer removeSignals() // ignore the error
if Paranoid { // TODO: this filtering might not be necessary anymore...
// try to handle the filtering inside this function!
rch := make(chan *dbus.Signal)
go func() {
loop:
for {
select {
case event := <-ch:
// "A receive from a closed channel returns the
// zero value immediately": if i get nil here,
// it means the channel was closed by someone!!
if event == nil { // shared bus issue?
obj.Logf("Hrm, channel was closed!")
break loop // TODO: continue?
}
// i think this was caused by using the shared
// bus, but we might as well leave it in for now
if event.Path != PkPath || event.Name != fmt.Sprintf("%s.%s", PkIface, signal) {
obj.Logf("Woops: Event: %+v", event)
continue
}
rch <- event // forward...
}
}
defer close(ch)
}()
return rch, nil
}
return ch, nil
}
// CreateTransaction creates and returns a transaction path.
func (obj *Conn) CreateTransaction() (dbus.ObjectPath, error) {
if obj.Debug {
obj.Logf("CreateTransaction()")
}
var interfacePath dbus.ObjectPath
bus := obj.GetBus().Object(PkIface, PkPath)
call := bus.Call(fmt.Sprintf("%s.CreateTransaction", PkIface), 0).Store(&interfacePath)
if call != nil {
return "", call
}
if obj.Debug {
obj.Logf("CreateTransaction(): %v", interfacePath)
}
return interfacePath, nil
}
// ResolvePackages runs the PackageKit Resolve method and returns the result.
func (obj *Conn) ResolvePackages(packages []string, filter uint64) ([]string, error) {
packageIDs := []string{}
ch := make(chan *dbus.Signal, PkBufferSize) // we need to buffer :(
interfacePath, err := obj.CreateTransaction() // emits Destroy on close
if err != nil {
return []string{}, err
}
// add signal matches for Package and Finished which will always be last
var signals = []string{"Package", "Finished", "Error", "Destroy"}
removeSignals, err := obj.matchSignal(ch, interfacePath, PkIfaceTransaction, signals)
if err != nil {
return nil, err
}
defer removeSignals()
if obj.Debug {
obj.Logf("ResolvePackages(): Object(%s, %v)", PkIface, interfacePath)
}
bus := obj.GetBus().Object(PkIface, interfacePath) // pass in found transaction path
call := bus.Call(FmtTransactionMethod("Resolve"), 0, filter, packages)
if obj.Debug {
obj.Logf("ResolvePackages(): Call: Success!")
}
if call.Err != nil {
return []string{}, call.Err
}
loop:
for {
// FIXME: add a timeout option to error in case signals are dropped!
select {
case signal := <-ch:
if obj.Debug {
obj.Logf("ResolvePackages(): Signal: %+v", signal)
}
if signal.Path != interfacePath {
obj.Logf("Woops: Signal.Path: %+v", signal.Path)
continue loop
}
if signal.Name == FmtTransactionMethod("Package") {
//pkg_int, ok := signal.Body[0].(int)
packageID, ok := signal.Body[1].(string)
// format is: name;version;arch;data
if !ok {
continue loop
}
//comment, ok := signal.Body[2].(string)
for _, p := range packageIDs {
if packageID == p {
continue loop // duplicate!
}
}
packageIDs = append(packageIDs, packageID)
} else if signal.Name == FmtTransactionMethod("Finished") {
// TODO: should we wait for the Destroy signal?
break loop
} else if signal.Name == FmtTransactionMethod("Destroy") {
// should already be broken
break loop
} else {
return []string{}, fmt.Errorf("error in body: %v", signal.Body)
}
}
}
return packageIDs, nil
}
// IsInstalledList queries a list of packages to see if they are installed.
func (obj *Conn) IsInstalledList(packages []string) ([]bool, error) {
var filter uint64 // initializes at the "zero" value of 0
filter += PkFilterEnumArch // always search in our arch
packageIDs, err := obj.ResolvePackages(packages, filter)
if err != nil {
return nil, errwrap.Wrapf(err, "error resolving packages")
}
var m = make(map[string]int)
for _, packageID := range packageIDs {
s := strings.Split(packageID, ";")
//if len(s) != 4 { continue } // this would be a bug!
pkg := s[0]
flags := strings.Split(s[3], ":")
for _, f := range flags {
if f == "installed" {
if _, exists := m[pkg]; !exists {
m[pkg] = 0
}
m[pkg]++ // if we see pkg installed, increment
break
}
}
}
var r []bool
for _, p := range packages {
if value, exists := m[p]; exists {
r = append(r, value > 0) // at least 1 means installed
} else {
r = append(r, false)
}
}
return r, nil
}
// IsInstalled returns if a package is installed.
// TODO: this could be optimized by making the resolve call directly
func (obj *Conn) IsInstalled(pkg string) (bool, error) {
p, e := obj.IsInstalledList([]string{pkg})
if len(p) != 1 {
return false, e
}
return p[0], nil
}
// InstallPackages installs a list of packages by packageID.
func (obj *Conn) InstallPackages(packageIDs []string, transactionFlags uint64) error {
ch := make(chan *dbus.Signal, PkBufferSize) // we need to buffer :(
interfacePath, err := obj.CreateTransaction() // emits Destroy on close
if err != nil {
return err
}
var signals = []string{"Package", "ErrorCode", "Finished", "Destroy"} // "ItemProgress", "Status" ?
removeSignals, err := obj.matchSignal(ch, interfacePath, PkIfaceTransaction, signals)
if err != nil {
return err
}
defer removeSignals()
bus := obj.GetBus().Object(PkIface, interfacePath) // pass in found transaction path
call := bus.Call(FmtTransactionMethod("RefreshCache"), 0, false)
if call.Err != nil {
return call.Err
}
call = bus.Call(FmtTransactionMethod("InstallPackages"), 0, transactionFlags, packageIDs)
if call.Err != nil {
return call.Err
}
timeout := -1 // disabled initially
finished := false
loop:
for {
select {
case signal := <-ch:
if signal.Path != interfacePath {
obj.Logf("Woops: Signal.Path: %+v", signal.Path)
continue loop
}
if signal.Name == FmtTransactionMethod("ErrorCode") {
return fmt.Errorf("error in body: %v", signal.Body)
} else if signal.Name == FmtTransactionMethod("Package") {
// a package was installed...
// only start the timer once we're here...
timeout = PkSignalPackageTimeout
} else if signal.Name == FmtTransactionMethod("Finished") {
finished = true
timeout = PkSignalDestroyTimeout // wait a bit
} else if signal.Name == FmtTransactionMethod("Destroy") {
return nil // success
} else {
return fmt.Errorf("error in body: %v", signal.Body)
}
case <-util.TimeAfterOrBlock(timeout):
if finished {
obj.Logf("Timeout: InstallPackages: Waiting for 'Destroy'")
return nil // got tired of waiting for Destroy
}
return fmt.Errorf("timeout installing packages: %s", strings.Join(packageIDs, ", "))
}
}
}
// RemovePackages removes a list of packages by packageID.
func (obj *Conn) RemovePackages(packageIDs []string, transactionFlags uint64) error {
var allowDeps = true // TODO: configurable
var autoremove = false // unsupported on GNU/Linux
ch := make(chan *dbus.Signal, PkBufferSize) // we need to buffer :(
interfacePath, err := obj.CreateTransaction() // emits Destroy on close
if err != nil {
return err
}
var signals = []string{"Package", "ErrorCode", "Finished", "Destroy"} // "ItemProgress", "Status" ?
removeSignals, err := obj.matchSignal(ch, interfacePath, PkIfaceTransaction, signals)
if err != nil {
return err
}
defer removeSignals()
bus := obj.GetBus().Object(PkIface, interfacePath) // pass in found transaction path
call := bus.Call(FmtTransactionMethod("RemovePackages"), 0, transactionFlags, packageIDs, allowDeps, autoremove)
if call.Err != nil {
return call.Err
}
loop:
for {
// FIXME: add a timeout option to error in case signals are dropped!
select {
case signal := <-ch:
if signal.Path != interfacePath {
obj.Logf("Woops: Signal.Path: %+v", signal.Path)
continue loop
}
if signal.Name == FmtTransactionMethod("ErrorCode") {
return fmt.Errorf("error in body: %v", signal.Body)
} else if signal.Name == FmtTransactionMethod("Package") {
// a package was installed...
continue loop
} else if signal.Name == FmtTransactionMethod("Finished") {
// TODO: should we wait for the Destroy signal?
break loop
} else if signal.Name == FmtTransactionMethod("Destroy") {
// should already be broken
break loop
} else {
return fmt.Errorf("error in body: %v", signal.Body)
}
}
}
return nil
}
// UpdatePackages updates a list of packages to versions that are specified.
func (obj *Conn) UpdatePackages(packageIDs []string, transactionFlags uint64) error {
ch := make(chan *dbus.Signal, PkBufferSize) // we need to buffer :(
interfacePath, err := obj.CreateTransaction()
if err != nil {
return err
}
var signals = []string{"Package", "ErrorCode", "Finished", "Destroy"} // "ItemProgress", "Status" ?
removeSignals, err := obj.matchSignal(ch, interfacePath, PkIfaceTransaction, signals)
if err != nil {
return err
}
defer removeSignals()
bus := obj.GetBus().Object(PkIface, interfacePath) // pass in found transaction path
call := bus.Call(FmtTransactionMethod("UpdatePackages"), 0, transactionFlags, packageIDs)
if call.Err != nil {
return call.Err
}
loop:
for {
// FIXME: add a timeout option to error in case signals are dropped!
select {
case signal := <-ch:
if signal.Path != interfacePath {
obj.Logf("Woops: Signal.Path: %+v", signal.Path)
continue loop
}
if signal.Name == FmtTransactionMethod("ErrorCode") {
return fmt.Errorf("error in body: %v", signal.Body)
} else if signal.Name == FmtTransactionMethod("Package") {
} else if signal.Name == FmtTransactionMethod("Finished") {
// TODO: should we wait for the Destroy signal?
break loop
} else if signal.Name == FmtTransactionMethod("Destroy") {
// should already be broken
break loop
} else {
return fmt.Errorf("error in body: %v", signal.Body)
}
}
}
return nil
}
// GetFilesByPackageID gets the list of files that are contained inside a list
// of packageIDs.
func (obj *Conn) GetFilesByPackageID(packageIDs []string) (files map[string][]string, err error) {
// NOTE: the maximum number of files in an RPM is 52116 in Fedora 23
// https://gist.github.com/purpleidea/b98e60dcd449e1ac3b8a
ch := make(chan *dbus.Signal, PkBufferSize) // we need to buffer :(
interfacePath, err := obj.CreateTransaction()
if err != nil {
return
}
var signals = []string{"Files", "ErrorCode", "Finished", "Destroy"} // "ItemProgress", "Status" ?
removeSignals, err := obj.matchSignal(ch, interfacePath, PkIfaceTransaction, signals)
if err != nil {
return
}
defer removeSignals()
bus := obj.GetBus().Object(PkIface, interfacePath) // pass in found transaction path
call := bus.Call(FmtTransactionMethod("GetFiles"), 0, packageIDs)
if call.Err != nil {
err = call.Err
return
}
files = make(map[string][]string)
loop:
for {
// FIXME: add a timeout option to error in case signals are dropped!
select {
case signal := <-ch:
if signal.Path != interfacePath {
obj.Logf("Woops: Signal.Path: %+v", signal.Path)
continue loop
}
if signal.Name == FmtTransactionMethod("ErrorCode") {
err = fmt.Errorf("error in body: %v", signal.Body)
return
// one signal returned per packageID found...
} else if signal.Name == FmtTransactionMethod("Files") {
if len(signal.Body) != 2 { // bad data
continue loop
}
var ok bool
var key string
var fileList []string
if key, ok = signal.Body[0].(string); !ok {
continue loop
}
if fileList, ok = signal.Body[1].([]string); !ok {
continue loop // failed conversion
}
files[key] = fileList // build up map
} else if signal.Name == FmtTransactionMethod("Finished") {
// TODO: should we wait for the Destroy signal?
break loop
} else if signal.Name == FmtTransactionMethod("Destroy") {
// should already be broken
break loop
} else {
err = fmt.Errorf("error in body: %v", signal.Body)
return
}
}
}
return
}
// GetUpdates gets a list of packages that are installed and which can be
// updated, mod filter.
func (obj *Conn) GetUpdates(filter uint64) ([]string, error) {
if obj.Debug {
obj.Logf("GetUpdates()")
}
packageIDs := []string{}
ch := make(chan *dbus.Signal, PkBufferSize) // we need to buffer :(
interfacePath, err := obj.CreateTransaction()
if err != nil {
return nil, err
}
var signals = []string{"Package", "ErrorCode", "Finished", "Destroy"} // "ItemProgress" ?
removeSignals, err := obj.matchSignal(ch, interfacePath, PkIfaceTransaction, signals)
if err != nil {
return nil, err
}
defer removeSignals()
bus := obj.GetBus().Object(PkIface, interfacePath) // pass in found transaction path
call := bus.Call(FmtTransactionMethod("GetUpdates"), 0, filter)
if call.Err != nil {
return nil, call.Err
}
loop:
for {
// FIXME: add a timeout option to error in case signals are dropped!
select {
case signal := <-ch:
if signal.Path != interfacePath {
obj.Logf("Woops: Signal.Path: %+v", signal.Path)
continue loop
}
if signal.Name == FmtTransactionMethod("ErrorCode") {
return nil, fmt.Errorf("error in body: %v", signal.Body)
} else if signal.Name == FmtTransactionMethod("Package") {
//pkg_int, ok := signal.Body[0].(int)
packageID, ok := signal.Body[1].(string)
// format is: name;version;arch;data
if !ok {
continue loop
}
//comment, ok := signal.Body[2].(string)
for _, p := range packageIDs { // optional?
if packageID == p {
continue loop // duplicate!
}
}
packageIDs = append(packageIDs, packageID)
} else if signal.Name == FmtTransactionMethod("Finished") {
// TODO: should we wait for the Destroy signal?
break loop
} else if signal.Name == FmtTransactionMethod("Destroy") {
// should already be broken
break loop
} else {
return nil, fmt.Errorf("error in body: %v", signal.Body)
}
}
}
return packageIDs, nil
}
// PackagesToPackageIDs is a helper function that *might* be generally useful
// outside mgmt. The packageMap input has the package names as keys and
// requested states as values. These states can be: installed, uninstalled,
// newest or a requested version str.
func (obj *Conn) PackagesToPackageIDs(packageMap map[string]string, filter uint64) (map[string]*PkPackageIDActionData, error) {
count := 0
packages := make([]string, len(packageMap))
for k := range packageMap { // lol, golang has no hash.keys() function!
packages[count] = k
count++
}
if !(filter&PkFilterEnumArch == PkFilterEnumArch) {
filter += PkFilterEnumArch // always search in our arch
}
if obj.Debug {
obj.Logf("PackagesToPackageIDs(): %s", strings.Join(packages, ", "))
}
resolved, err := obj.ResolvePackages(packages, filter)
if err != nil {
return nil, errwrap.Wrapf(err, "error resolving")
}
found := make([]bool, count) // default false
installed := make([]bool, count)
version := make([]string, count)
usePackageID := make([]string, count)
newest := make([]bool, count) // default true
for i := range newest {
newest[i] = true // assume, for now
}
var index int
for _, packageID := range resolved {
index = -1
//obj.Logf("* %v", packageID)
// format is: name;version;arch;data
s := strings.Split(packageID, ";")
//if len(s) != 4 { continue } // this would be a bug!
pkg, ver, arch, data := s[0], s[1], s[2], s[3]
// we might need to allow some of this, eg: i386 .deb on amd64
b, err := IsMyArch(arch)
if err != nil {
return nil, errwrap.Wrapf(err, "arch error")
} else if !b {
continue
}
for i := range packages { // find pkg if it exists
if pkg == packages[i] {
index = i
}
}
if index == -1 { // can't find what we're looking for
continue
}
state := packageMap[pkg] // lookup the requested state/version
if state == "" {
return nil, fmt.Errorf("empty package state for: `%s`", pkg)
}
found[index] = true
stateIsVersion := (state != "installed" && state != "uninstalled" && state != "newest") // must be a ver. string
if stateIsVersion {
if state == ver && ver != "" { // we match what we want...
usePackageID[index] = packageID
}
}
if FlagInData("installed", data) {
installed[index] = true
version[index] = ver
// state of "uninstalled" matched during CheckApply, and
// states of "installed" and "newest" for fileList
if !stateIsVersion {
usePackageID[index] = packageID // save for later
}
} else { // not installed...
if !stateIsVersion {
// if there is more than one result, eg: there
// is the old and newest version of a package,
// then this section can run more than once...
// in that case, don't worry, we'll choose the
// right value in the "updates" section below!
usePackageID[index] = packageID
}
}
}
// we can't determine which packages are "newest", without searching
// for each one individually, so instead we check if any updates need
// to be done, and if so, anything that needs updating isn't newest!
// if something isn't installed, we can't verify it with this method
// FIXME: https://github.com/hughsie/PackageKit/issues/116
updates, err := obj.GetUpdates(filter)
if err != nil {
return nil, errwrap.Wrapf(err, "updates error")
}
for _, packageID := range updates {
//obj.Logf("* %v", packageID)
// format is: name;version;arch;data
s := strings.Split(packageID, ";")
//if len(s) != 4 { continue } // this would be a bug!
pkg, _, _, _ := s[0], s[1], s[2], s[3]
for index := range packages { // find pkg if it exists
if pkg == packages[index] {
state := packageMap[pkg] // lookup
newest[index] = false
if state == "installed" || state == "newest" {
// fix up in case above wasn't correct!
usePackageID[index] = packageID
}
break
}
}
}
// skip if the "newest" filter was used, otherwise we might need fixing
// this check is for packages that need to verify their "newest" status
// we need to know this so we can install the correct newest packageID!
recursion := make(map[string]*PkPackageIDActionData)
if !(filter&PkFilterEnumNewest == PkFilterEnumNewest) {
checkPackages := []string{}
filteredPackageMap := make(map[string]string)
for index, pkg := range packages {
state := packageMap[pkg] // lookup the requested state/version
if !found[index] || installed[index] { // skip these, they're okay
continue
}
if !(state == "newest" || state == "installed") {
continue
}
checkPackages = append(checkPackages, pkg)
filteredPackageMap[pkg] = packageMap[pkg] // check me!
}
// we _could_ do a second resolve and then parse like this...
//resolved, e := obj.ResolvePackages(..., filter+PkFilterEnumNewest)
// but that's basically what recursion here could do too!
if len(checkPackages) > 0 {
if obj.Debug {
obj.Logf("PackagesToPackageIDs(): Recurse: %s", strings.Join(checkPackages, ", "))
}
recursion, err = obj.PackagesToPackageIDs(filteredPackageMap, filter+PkFilterEnumNewest)
if err != nil {
return nil, errwrap.Wrapf(err, "recursion error")
}
}
}
// fix up and build result format
result := make(map[string]*PkPackageIDActionData)
for index, pkg := range packages {
if !found[index] || !installed[index] {
newest[index] = false // make the results more logical!
}
// prefer recursion results if present
if lookup, ok := recursion[pkg]; ok {
result[pkg] = lookup
} else {
result[pkg] = &PkPackageIDActionData{
Found: found[index],
Installed: installed[index],
Version: version[index],
PackageID: usePackageID[index],
Newest: newest[index],
}
}
}
return result, nil
}
// FilterPackageIDs returns a list of packageIDs which match the set of package
// names in packages.
func FilterPackageIDs(m map[string]*PkPackageIDActionData, packages []string) ([]string, error) {
result := []string{}
for _, k := range packages {
p, ok := m[k] // lookup single package
// package doesn't exist, this is an error!
if !ok || !p.Found || p.PackageID == "" {
return nil, fmt.Errorf("can't find package named '%s'", k)
}
result = append(result, p.PackageID)
}
return result, nil
}
// FilterState returns a map of whether each package queried matches the
// particular state.
func FilterState(m map[string]*PkPackageIDActionData, packages []string, state string) (result map[string]bool, err error) {
result = make(map[string]bool)
pkgs := []string{} // bad pkgs that don't have a bool state
for _, k := range packages {
p, ok := m[k] // lookup single package
// package doesn't exist, this is an error!
if !ok || !p.Found {
return nil, fmt.Errorf("can't find package named '%s'", k)
}
var b bool
if state == "installed" {
b = p.Installed
} else if state == "uninstalled" {
b = !p.Installed
} else if state == "newest" {
b = p.Newest
} else {
// we can't filter "version" state in this function
pkgs = append(pkgs, k)
continue
}
result[k] = b // save
}
if len(pkgs) > 0 {
err = fmt.Errorf("can't filter non-boolean state on: %s", strings.Join(pkgs, ","))
}
return result, err
}
// FilterPackageState returns all packages that are in package and match the
// specific state.
func FilterPackageState(m map[string]*PkPackageIDActionData, packages []string, state string) (result []string, err error) {
result = []string{}
for _, k := range packages {
p, ok := m[k] // lookup single package
// package doesn't exist, this is an error!
if !ok || !p.Found {
return nil, fmt.Errorf("can't find package named '%s'", k)
}
b := false
if state == "installed" && p.Installed {
b = true
} else if state == "uninstalled" && !p.Installed {
b = true
} else if state == "newest" && p.Newest {
b = true
} else if state == p.Version {
b = true
}
if b {
result = append(result, k)
}
}
return result, err
}
// FlagInData asks whether a flag exists inside the data portion of a packageID
// field?
func FlagInData(flag, data string) bool {
flags := strings.Split(data, ":")
for _, f := range flags {
if f == flag {
return true
}
}
return false
}
// FmtTransactionMethod builds the transaction method string properly.
func FmtTransactionMethod(method string) string {
return fmt.Sprintf("%s.%s", PkIfaceTransaction, method)
}
// IsMyArch determines if a PackageKit architecture matches the current os arch.
func IsMyArch(arch string) (bool, error) {
goarch, ok := archUtil.MapPackageKitArchToGoArch[arch]
if !ok {
// if you get this error, please update the PkArchMap const
return false, fmt.Errorf("arch '%s', not found", arch)
}
if goarch == archUtil.Any { // special value that corresponds to noarch
return true, nil
}
return goarch == runtime.GOARCH, nil
}