998 lines
32 KiB
Go
998 lines
32 KiB
Go
// Mgmt
|
|
// Copyright (C) 2013-2024+ 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"
|
|
"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"
|
|
)
|
|
|
|
var (
|
|
// PkArchMap contains the mapping from PackageKit arch to GOARCH.
|
|
// GOARCH's: 386, amd64, arm, arm64, mips64, mips64le, ppc64, ppc64le
|
|
PkArchMap = map[string]string{ // map of PackageKit arch to GOARCH
|
|
// TODO: add more values
|
|
// noarch
|
|
"noarch": "ANY", // special value "ANY" (noarch as seen in Fedora)
|
|
"any": "ANY", // special value "ANY" ('any' as seen in ArchLinux)
|
|
"all": "ANY", // special value "ANY" ('all' as seen in Debian)
|
|
// fedora
|
|
"x86_64": "amd64",
|
|
"aarch64": "arm64",
|
|
// debian, from: https://www.debian.org/ports/
|
|
"amd64": "amd64",
|
|
"arm64": "arm64",
|
|
"i386": "386",
|
|
"i486": "386",
|
|
"i586": "386",
|
|
"i686": "386",
|
|
}
|
|
)
|
|
|
|
// 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 := PkArchMap[arch]
|
|
if !ok {
|
|
// if you get this error, please update the PkArchMap const
|
|
return false, fmt.Errorf("arch '%s', not found", arch)
|
|
}
|
|
if goarch == "ANY" { // special value that corresponds to noarch
|
|
return true, nil
|
|
}
|
|
return goarch == runtime.GOARCH, nil
|
|
}
|