Files
mgmt/packagekit.go
James Shubin c999f0c2cd Add initial "autoedge" plumbing
This allows for resources to automatically add necessary edges to the
graph so that the event system doesn't have to work overtime due to
sub-optimal execution order.
2016-03-10 03:29:50 -05:00

545 lines
18 KiB
Go

// Mgmt
// Copyright (C) 2013-2016+ 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 Affero 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 Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
// DOCS: https://www.freedesktop.org/software/PackageKit/gtk-doc/index.html
//package packagekit // TODO
package main
import (
"errors"
"fmt"
"github.com/godbus/dbus"
"log"
"strings"
)
const (
PK_DEBUG = false
PARANOID = false // enable if you see any ghosts
)
const (
PkBufferSize = 100
PkPath = "/org/freedesktop/PackageKit"
PkIface = "org.freedesktop.PackageKit"
PkIfaceTransaction = PkIface + ".Transaction"
dbusAddMatch = "org.freedesktop.DBus.AddMatch"
)
var (
PkArchMap = map[string]string{ // map of PackageKit arch to GOARCH
"x86_64": "amd64",
// TODO: add more values
}
)
//type enum_filter uint64
// https://github.com/hughsie/PackageKit/blob/master/lib/packagekit-glib2/pk-enum.c
const ( //static const PkEnumMatch enum_filter[]
PK_FILTER_ENUM_UNKNOWN uint64 = 1 << iota // "unknown"
PK_FILTER_ENUM_NONE // "none"
PK_FILTER_ENUM_INSTALLED // "installed"
PK_FILTER_ENUM_NOT_INSTALLED // "~installed"
PK_FILTER_ENUM_DEVELOPMENT // "devel"
PK_FILTER_ENUM_NOT_DEVELOPMENT // "~devel"
PK_FILTER_ENUM_GUI // "gui"
PK_FILTER_ENUM_NOT_GUI // "~gui"
PK_FILTER_ENUM_FREE // "free"
PK_FILTER_ENUM_NOT_FREE // "~free"
PK_FILTER_ENUM_VISIBLE // "visible"
PK_FILTER_ENUM_NOT_VISIBLE // "~visible"
PK_FILTER_ENUM_SUPPORTED // "supported"
PK_FILTER_ENUM_NOT_SUPPORTED // "~supported"
PK_FILTER_ENUM_BASENAME // "basename"
PK_FILTER_ENUM_NOT_BASENAME // "~basename"
PK_FILTER_ENUM_NEWEST // "newest"
PK_FILTER_ENUM_NOT_NEWEST // "~newest"
PK_FILTER_ENUM_ARCH // "arch"
PK_FILTER_ENUM_NOT_ARCH // "~arch"
PK_FILTER_ENUM_SOURCE // "source"
PK_FILTER_ENUM_NOT_SOURCE // "~source"
PK_FILTER_ENUM_COLLECTIONS // "collections"
PK_FILTER_ENUM_NOT_COLLECTIONS // "~collections"
PK_FILTER_ENUM_APPLICATION // "application"
PK_FILTER_ENUM_NOT_APPLICATION // "~application"
PK_FILTER_ENUM_DOWNLOADED // "downloaded"
PK_FILTER_ENUM_NOT_DOWNLOADED // "~downloaded"
)
const ( //static const PkEnumMatch enum_transaction_flag[]
PK_TRANSACTION_FLAG_ENUM_NONE uint64 = 1 << iota // "none"
PK_TRANSACTION_FLAG_ENUM_ONLY_TRUSTED // "only-trusted"
PK_TRANSACTION_FLAG_ENUM_SIMULATE // "simulate"
PK_TRANSACTION_FLAG_ENUM_ONLY_DOWNLOAD // "only-download"
PK_TRANSACTION_FLAG_ENUM_ALLOW_REINSTALL // "allow-reinstall"
PK_TRANSACTION_FLAG_ENUM_JUST_REINSTALL // "just-reinstall"
PK_TRANSACTION_FLAG_ENUM_ALLOW_DOWNGRADE // "allow-downgrade"
)
// wrapper struct so we can pass bus connection around in the struct
type Conn struct {
conn *dbus.Conn
}
// get a new bus connection
func NewBus() *Conn {
// if we share the bus with others, we will get each others messages!!
bus, err := SystemBusPrivateUsable() // don't share the bus connection!
if err != nil {
return nil
}
return &Conn{
conn: bus,
}
}
// get the dbus connection object
func (bus *Conn) GetBus() *dbus.Conn {
return bus.conn
}
// close the dbus connection object
func (bus *Conn) Close() error {
return bus.conn.Close()
}
// internal helper to add signal matches to the bus, should only be called once
func (bus *Conn) matchSignal(ch chan *dbus.Signal, path dbus.ObjectPath, iface string, signals []string) error {
if PK_DEBUG {
log.Printf("PackageKit: matchSignal(%v, %v, %v, %v)", ch, path, iface, signals)
}
// eg: gdbus monitor --system --dest org.freedesktop.PackageKit --object-path /org/freedesktop/PackageKit | grep <signal>
var call *dbus.Call
// TODO: if we make this call many times, we seem to receive signals
// that many times... Maybe this should be an object singleton?
obj := bus.GetBus().BusObject()
pathStr := fmt.Sprintf("%s", path)
if len(signals) == 0 {
call = obj.Call(dbusAddMatch, 0, "type='signal',path='"+pathStr+"',interface='"+iface+"'")
} else {
for _, signal := range signals {
call = obj.Call(dbusAddMatch, 0, "type='signal',path='"+pathStr+"',interface='"+iface+"',member='"+signal+"'")
if call.Err != nil {
break
}
}
}
if call.Err != nil {
return call.Err
}
if PK_DEBUG {
log.Println("PackageKit: matchSignal(): Added!")
}
// 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!
bus.GetBus().Signal(ch)
if PK_DEBUG {
log.Println("PackageKit: matchSignal(): Success!")
}
return nil
}
// get a signal anytime an event happens
func (bus *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"
err := bus.matchSignal(ch, PkPath, PkIface, []string{signal})
if err != nil {
return nil, err
}
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?
log.Println("PackageKit: 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) {
log.Println("PackageKit: Some wires have been crossed!")
continue
}
rch <- event // forward...
}
}
defer close(ch)
}()
return rch, nil
}
return ch, nil
}
// create and return a transaction path
func (bus *Conn) CreateTransaction() (dbus.ObjectPath, error) {
if PK_DEBUG {
log.Println("PackageKit: CreateTransaction()")
}
var interfacePath dbus.ObjectPath
obj := bus.GetBus().Object(PkIface, PkPath)
call := obj.Call(fmt.Sprintf("%s.CreateTransaction", PkIface), 0).Store(&interfacePath)
if call != nil {
return "", call
}
if PK_DEBUG {
log.Printf("PackageKit: CreateTransaction(): %v", interfacePath)
}
return interfacePath, nil
}
func (bus *Conn) ResolvePackages(packages []string, filter uint64) ([]string, error) {
packageIds := []string{}
ch := make(chan *dbus.Signal, PkBufferSize) // we need to buffer :(
interfacePath, err := bus.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"}
bus.matchSignal(ch, interfacePath, PkIfaceTransaction, signals)
if PK_DEBUG {
log.Printf("PackageKit: ResolvePackages(): Object(%v, %v)", PkIface, interfacePath)
}
obj := bus.GetBus().Object(PkIface, interfacePath) // pass in found transaction path
call := obj.Call(FmtTransactionMethod("Resolve"), 0, filter, packages)
if PK_DEBUG {
log.Println("PackageKit: 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 PK_DEBUG {
log.Printf("PackageKit: ResolvePackages(): Signal: %+v", signal)
}
if signal.Path != interfacePath {
log.Println("PackageKit: Some wires have been crossed!")
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{}, errors.New(fmt.Sprintf("PackageKit error: %v", signal.Body))
}
}
}
return packageIds, nil
}
func (bus *Conn) IsInstalledList(packages []string) ([]bool, error) {
var filter uint64 = 0
filter += PK_FILTER_ENUM_ARCH // always search in our arch
packageIds, e := bus.ResolvePackages(packages, filter)
if e != nil {
return nil, errors.New(fmt.Sprintf("ResolvePackages error: %v", e))
}
var m map[string]int = 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
}
// is package installed ?
// TODO: this could be optimized by making the resolve call directly
func (bus *Conn) IsInstalled(pkg string) (bool, error) {
p, e := bus.IsInstalledList([]string{pkg})
if len(p) != 1 {
return false, e
}
return p[0], nil
}
// install list of packages by packageId
func (bus *Conn) InstallPackages(packageIds []string, transactionFlags uint64) error {
ch := make(chan *dbus.Signal, PkBufferSize) // we need to buffer :(
interfacePath, err := bus.CreateTransaction() // emits Destroy on close
if err != nil {
return err
}
var signals = []string{"Package", "ErrorCode", "Finished", "Destroy"} // "ItemProgress", "Status" ?
bus.matchSignal(ch, interfacePath, PkIfaceTransaction, signals)
obj := bus.GetBus().Object(PkIface, interfacePath) // pass in found transaction path
call := obj.Call(FmtTransactionMethod("InstallPackages"), 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 {
log.Println("PackageKit: Some wires have been crossed!")
continue loop
}
if signal.Name == FmtTransactionMethod("ErrorCode") {
return errors.New(fmt.Sprintf("PackageKit error: %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 errors.New(fmt.Sprintf("PackageKit error: %v", signal.Body))
}
}
}
return nil
}
// remove list of packages
func (bus *Conn) RemovePackages(packageIds []string, transactionFlags uint64) error {
var allowDeps bool = true // TODO: configurable
var autoremove bool = false // unsupported on GNU/Linux
ch := make(chan *dbus.Signal, PkBufferSize) // we need to buffer :(
interfacePath, err := bus.CreateTransaction() // emits Destroy on close
if err != nil {
return err
}
var signals = []string{"Package", "ErrorCode", "Finished", "Destroy"} // "ItemProgress", "Status" ?
bus.matchSignal(ch, interfacePath, PkIfaceTransaction, signals)
obj := bus.GetBus().Object(PkIface, interfacePath) // pass in found transaction path
call := obj.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 {
log.Println("PackageKit: Some wires have been crossed!")
continue loop
}
if signal.Name == FmtTransactionMethod("ErrorCode") {
return errors.New(fmt.Sprintf("PackageKit error: %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 errors.New(fmt.Sprintf("PackageKit error: %v", signal.Body))
}
}
}
return nil
}
// update list of packages to versions that are specified
func (bus *Conn) UpdatePackages(packageIds []string, transactionFlags uint64) error {
ch := make(chan *dbus.Signal, PkBufferSize) // we need to buffer :(
interfacePath, err := bus.CreateTransaction()
if err != nil {
return err
}
var signals = []string{"Package", "ErrorCode", "Finished", "Destroy"} // "ItemProgress", "Status" ?
bus.matchSignal(ch, interfacePath, PkIfaceTransaction, signals)
obj := bus.GetBus().Object(PkIface, interfacePath) // pass in found transaction path
call := obj.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 {
log.Println("PackageKit: Some wires have been crossed!")
continue loop
}
if signal.Name == FmtTransactionMethod("ErrorCode") {
return errors.New(fmt.Sprintf("PackageKit error: %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 errors.New(fmt.Sprintf("PackageKit error: %v", signal.Body))
}
}
}
return nil
}
// get the list of files that are contained inside a list of packageids
func (bus *Conn) GetFilesByPackageId(packageIds []string) (files map[string][]string, err error) {
ch := make(chan *dbus.Signal, PkBufferSize) // we need to buffer :(
interfacePath, err := bus.CreateTransaction()
if err != nil {
return
}
var signals = []string{"Files", "ErrorCode", "Finished", "Destroy"} // "ItemProgress", "Status" ?
bus.matchSignal(ch, interfacePath, PkIfaceTransaction, signals)
obj := bus.GetBus().Object(PkIface, interfacePath) // pass in found transaction path
call := obj.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 {
log.Println("PackageKit: Some wires have been crossed!")
continue loop
}
if signal.Name == FmtTransactionMethod("ErrorCode") {
err = errors.New(fmt.Sprintf("PackageKit error: %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
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 {
err = errors.New(fmt.Sprintf("PackageKit error: %v", signal.Body))
return
}
}
}
return
}
// does flag exist inside data portion of packageId field?
func FlagInData(flag, data string) bool {
flags := strings.Split(data, ":")
for _, f := range flags {
if f == flag {
return true
}
}
return false
}
// builds the transaction method string
func FmtTransactionMethod(method string) string {
return fmt.Sprintf("%s.%s", PkIfaceTransaction, method)
}