diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..6ba39d5 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,18 @@ +language: go +dist: xenial + +go: + - "1.10.x" + - "1.11.x" + - "1.x" + - master + +os: + - linux + - osx + +before_install: + - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get -qq update ; fi + - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get install -y libcups2-dev libavahi-client-dev ; fi + - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update ; fi + - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew install bazaar ; fi diff --git a/README.md b/README.md index 0234dc9..8b568d3 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,17 @@ # Google Cloud Print Connector ## Introduction -Share printers from your Windows, Linux, or OS X computer with ChromeOS and Android devices, using the Cloud Print Connector. The Connector is a purpose-built system process. It can share hundreds of printers on a powerful server, or one printer on a Raspberry Pi. +Share printers from your Windows, Linux, FreeBSD or OS X computer with ChromeOS and Android devices, using the Cloud Print Connector. The Connector is a purpose-built system process. It can share hundreds of printers on a powerful server, or one printer on a Raspberry Pi. -Lots of help can be found in [the wiki](https://github.com/google/cups-connector/wiki). +Lots of help can be found in [the wiki](https://github.com/google/cloud-print-connector/wiki). + +## Mailing list +Please join the mailing list at https://groups.google.com/forum/#!forum/cloud-print-connector. Anyone can post and view messages. + +## Build Status +* Linux/OSX: [![Build Status](https://travis-ci.org/google/cloud-print-connector.svg?branch=master)](https://travis-ci.org/google/cloud-print-connector) + +* FreeBSD: [![Build Status](http://jenkins.mouf.net/job/cloud-print-connector/badge/icon)](http://jenkins.mouf.net/job/cloud-print-connector/) ## License Copyright 2015 Google Inc. All rights reserved. diff --git a/cdd/cdd.go b/cdd/cdd.go index 5abc4b5..f44e276 100644 --- a/cdd/cdd.go +++ b/cdd/cdd.go @@ -316,8 +316,7 @@ type TypedValueCapability struct { } type Color struct { - Option []ColorOption `json:"option"` - VendorKey string `json:"-"` + Option []ColorOption `json:"option"` } type ColorType string @@ -339,8 +338,7 @@ type ColorOption struct { } type Duplex struct { - Option []DuplexOption `json:"option"` - VendorKey string `json:"-"` + Option []DuplexOption `json:"option"` } type DuplexType string @@ -354,7 +352,6 @@ const ( type DuplexOption struct { Type DuplexType `json:"type"` // default = "NO_DUPLEX" IsDefault bool `json:"is_default"` // default = false - VendorID string `json:"-"` } type PageOrientation struct { diff --git a/cups/core.go b/cups/core.go index 3fd8208..f9ac5f7 100644 --- a/cups/core.go +++ b/cups/core.go @@ -4,11 +4,13 @@ // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd -// +build linux darwin +// +build linux darwin freebsd package cups /* +#cgo freebsd CFLAGS: -I/usr/local/include +#cgo freebsd LDFLAGS: -L/usr/local/lib #include "cups.h" */ import "C" @@ -21,8 +23,8 @@ import ( "time" "unsafe" - "github.com/google/cups-connector/lib" - "github.com/google/cups-connector/log" + "github.com/google/cloud-print-connector/lib" + "github.com/google/cloud-print-connector/log" ) const ( diff --git a/cups/cups.go b/cups/cups.go index d33ddb6..efa3361 100644 --- a/cups/cups.go +++ b/cups/cups.go @@ -4,7 +4,7 @@ // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd -// +build linux darwin +// +build linux darwin freebsd package cups @@ -26,9 +26,9 @@ import ( "time" "unsafe" - "github.com/google/cups-connector/cdd" - "github.com/google/cups-connector/lib" - "github.com/google/cups-connector/log" + "github.com/google/cloud-print-connector/cdd" + "github.com/google/cloud-print-connector/lib" + "github.com/google/cloud-print-connector/log" ) const ( @@ -136,11 +136,15 @@ type CUPS struct { printerAttributes []string systemTags map[string]string printerBlacklist map[string]interface{} + printerWhitelist map[string]interface{} ignoreRawPrinters bool ignoreClassPrinters bool } -func NewCUPS(infoToDisplayName, prefixJobIDToJobTitle bool, displayNamePrefix string, printerAttributes []string, maxConnections uint, connectTimeout time.Duration, printerBlacklist []string, ignoreRawPrinters bool, ignoreClassPrinters bool) (*CUPS, error) { +func NewCUPS(infoToDisplayName, prefixJobIDToJobTitle bool, displayNamePrefix string, + printerAttributes, vendorPPDOptions []string, maxConnections uint, connectTimeout time.Duration, + printerBlacklist, printerWhitelist []string, ignoreRawPrinters bool, ignoreClassPrinters bool, + fcmNotificationsEnable bool) (*CUPS, error) { if err := checkPrinterAttributes(printerAttributes); err != nil { return nil, err } @@ -149,9 +153,9 @@ func NewCUPS(infoToDisplayName, prefixJobIDToJobTitle bool, displayNamePrefix st if err != nil { return nil, err } - pc := newPPDCache(cc) + pc := newPPDCache(cc, vendorPPDOptions) - systemTags, err := getSystemTags() + systemTags, err := getSystemTags(fcmNotificationsEnable) if err != nil { return nil, err } @@ -161,6 +165,11 @@ func NewCUPS(infoToDisplayName, prefixJobIDToJobTitle bool, displayNamePrefix st pb[p] = struct{}{} } + pw := map[string]interface{}{} + for _, p := range printerWhitelist { + pw[p] = struct{}{} + } + c := &CUPS{ cc: cc, pc: pc, @@ -169,6 +178,7 @@ func NewCUPS(infoToDisplayName, prefixJobIDToJobTitle bool, displayNamePrefix st printerAttributes: printerAttributes, systemTags: systemTags, printerBlacklist: pb, + printerWhitelist: pw, ignoreRawPrinters: ignoreRawPrinters, ignoreClassPrinters: ignoreClassPrinters, } @@ -212,7 +222,7 @@ func (c *CUPS) GetPrinters() ([]lib.Printer, error) { } printers := c.responseToPrinters(response) - printers = c.filterBlacklistPrinters(printers) + if c.ignoreRawPrinters { printers = filterRawPrinters(printers) } @@ -241,6 +251,26 @@ func (c *CUPS) responseToPrinters(response *C.ipp_t) []lib.Printer { } mAttributes := attributesToMap(attributes) pds, pss, name, defaultDisplayName, uuid, tags := translateAttrs(mAttributes) + + // Check whitelist/blacklist in loop once we have printer name. + // Avoids unnecessary processing of excluded printers. + if _, exists := c.printerBlacklist[name]; exists { + log.Debugf("Ignoring blacklisted printer %s", name) + if a == nil { + break + } + continue + } + if len(c.printerWhitelist) != 0 { + if _, exists := c.printerWhitelist[name]; !exists { + log.Debugf("Ignoring non-whitelisted printer %s", name) + if a == nil { + break + } + continue + } + } + if !c.infoToDisplayName || defaultDisplayName == "" { defaultDisplayName = name } @@ -263,27 +293,17 @@ func (c *CUPS) responseToPrinters(response *C.ipp_t) []lib.Printer { return printers } -func (c *CUPS) filterBlacklistPrinters(printers []lib.Printer) []lib.Printer { +// filterClassPrinters removes class printers from the slice. +func filterClassPrinters(printers []lib.Printer) []lib.Printer { result := make([]lib.Printer, 0, len(printers)) for i := range printers { - if _, exists := c.printerBlacklist[printers[i].Name]; !exists { + if !lib.PrinterIsClass(printers[i]) { result = append(result, printers[i]) } } return result } -// filterClassPrinters removes class printers from the slice. -func filterClassPrinters(printers []lib.Printer) []lib.Printer { - result := make([]lib.Printer, 0, len(printers)) - for i := range printers { - if !lib.PrinterIsClass(printers[i]) { - result = append(result, printers[i]) - } - } - return result -} - // filterRawPrinters removes raw printers from the slice. func filterRawPrinters(printers []lib.Printer) []lib.Printer { result := make([]lib.Printer, 0, len(printers)) @@ -304,10 +324,13 @@ func (c *CUPS) addPPDDescriptionToPrinters(printers []lib.Printer) []lib.Printer for i := range printers { wg.Add(1) go func(p *lib.Printer) { - if description, manufacturer, model, err := c.pc.getPPDCacheEntry(p.Name); err == nil { + if description, manufacturer, model, duplexMap, err := c.pc.getPPDCacheEntry(p.Name); err == nil { p.Description.Absorb(description) p.Manufacturer = manufacturer p.Model = model + if duplexMap != nil { + p.DuplexMap = duplexMap + } ch <- p } else { log.ErrorPrinter(p.Name, err) @@ -365,7 +388,7 @@ func uname() (string, string, string, string, string, error) { C.GoString(&name.machine[0]), nil } -func getSystemTags() (map[string]string, error) { +func getSystemTags(fcmNotificationsEnable bool) (map[string]string, error) { tags := make(map[string]string) tags["connector-version"] = lib.BuildDate @@ -375,7 +398,11 @@ func getSystemTags() (map[string]string, error) { } tags["system-arch"] = runtime.GOARCH tags["system-golang-version"] = runtime.Version() - + if fcmNotificationsEnable { + tags["system-notifications-channel"] = "fcm" + } else { + tags["system-notifications-channel"] = "xmpp" + } sysname, nodename, release, version, machine, err := uname() if err != nil { return nil, fmt.Errorf("CUPS failed to call uname while initializing: %s", err) @@ -628,3 +655,9 @@ func checkPrinterAttributes(printerAttributes []string) error { return nil } + +// The following functions are not relevant to CUPS printing, but are required by the NativePrintSystem interface. + +func (c *CUPS) ReleaseJob(printerName string, jobID uint32) error { + return nil +} diff --git a/cups/cups.h b/cups/cups.h index effa6a5..5a3eaa7 100644 --- a/cups/cups.h +++ b/cups/cups.h @@ -12,6 +12,7 @@ license that can be found in the LICENSE file or at #define _IPP_PRIVATE_STRUCTURES 1 #include +#include #include // size_t #include // free, calloc, malloc #include // AF_UNSPEC diff --git a/cups/ppdcache.go b/cups/ppdcache.go index bebb332..139b843 100644 --- a/cups/ppdcache.go +++ b/cups/ppdcache.go @@ -4,7 +4,7 @@ // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd -// +build linux darwin +// +build linux darwin freebsd package cups @@ -19,7 +19,8 @@ import ( "sync" "unsafe" - "github.com/google/cups-connector/cdd" + "github.com/google/cloud-print-connector/cdd" + "github.com/google/cloud-print-connector/lib" ) // This isn't really a cache, but an interface to CUPS' quirky PPD interface. @@ -30,16 +31,18 @@ import ( // (1) maintains temporary file copies of PPDs for each printer // (2) updates those PPD files as necessary type ppdCache struct { - cc *cupsCore - cache map[string]*ppdCacheEntry - cacheMutex sync.RWMutex + cc *cupsCore + vendorPPDOptions []string + cache map[string]*ppdCacheEntry + cacheMutex sync.RWMutex } -func newPPDCache(cc *cupsCore) *ppdCache { +func newPPDCache(cc *cupsCore, vendorPPDOptions []string) *ppdCache { cache := make(map[string]*ppdCacheEntry) pc := ppdCache{ - cc: cc, - cache: cache, + cc: cc, + vendorPPDOptions: vendorPPDOptions, + cache: cache, } return &pc } @@ -65,7 +68,7 @@ func (pc *ppdCache) removePPD(printername string) { } } -func (pc *ppdCache) getPPDCacheEntry(printername string) (*cdd.PrinterDescriptionSection, string, string, error) { +func (pc *ppdCache) getPPDCacheEntry(printername string) (*cdd.PrinterDescriptionSection, string, string, lib.DuplexVendorMap, error) { pc.cacheMutex.RLock() pce, exists := pc.cache[printername] pc.cacheMutex.RUnlock() @@ -73,11 +76,11 @@ func (pc *ppdCache) getPPDCacheEntry(printername string) (*cdd.PrinterDescriptio if !exists { pce, err := createPPDCacheEntry(printername) if err != nil { - return nil, "", "", err + return nil, "", "", nil, err } - if err = pce.refresh(pc.cc); err != nil { + if err = pce.refresh(pc.cc, pc.vendorPPDOptions); err != nil { pce.free() - return nil, "", "", err + return nil, "", "", nil, err } pc.cacheMutex.Lock() @@ -89,17 +92,17 @@ func (pc *ppdCache) getPPDCacheEntry(printername string) (*cdd.PrinterDescriptio go firstPCE.free() } pc.cache[printername] = pce - description, manufacturer, model := pce.getFields() - return &description, manufacturer, model, nil + description, manufacturer, model, duplexMap := pce.getFields() + return &description, manufacturer, model, duplexMap, nil } else { - if err := pce.refresh(pc.cc); err != nil { + if err := pce.refresh(pc.cc, pc.vendorPPDOptions); err != nil { delete(pc.cache, printername) pce.free() - return nil, "", "", err + return nil, "", "", nil, err } - description, manufacturer, model := pce.getFields() - return &description, manufacturer, model, nil + description, manufacturer, model, duplexMap := pce.getFields() + return &description, manufacturer, model, duplexMap, nil } } @@ -110,6 +113,7 @@ type ppdCacheEntry struct { description cdd.PrinterDescriptionSection manufacturer string model string + duplexMap lib.DuplexVendorMap mutex sync.Mutex } @@ -127,10 +131,10 @@ func createPPDCacheEntry(name string) (*ppdCacheEntry, error) { // getFields gets externally-interesting fields from this ppdCacheEntry under // a lock. The description is passed as a value (copy), to protect the cached copy. -func (pce *ppdCacheEntry) getFields() (cdd.PrinterDescriptionSection, string, string) { +func (pce *ppdCacheEntry) getFields() (cdd.PrinterDescriptionSection, string, string, lib.DuplexVendorMap) { pce.mutex.Lock() defer pce.mutex.Unlock() - return pce.description, pce.manufacturer, pce.model + return pce.description, pce.manufacturer, pce.model, pce.duplexMap } // free frees the memory that stores the name and buffer fields, and deletes @@ -145,7 +149,7 @@ func (pce *ppdCacheEntry) free() { // refresh calls cupsGetPPD3() to refresh this PPD information, in // case CUPS has a new PPD for the printer. -func (pce *ppdCacheEntry) refresh(cc *cupsCore) error { +func (pce *ppdCacheEntry) refresh(cc *cupsCore, vendorPPDOptions []string) error { pce.mutex.Lock() defer pce.mutex.Unlock() @@ -176,7 +180,7 @@ func (pce *ppdCacheEntry) refresh(cc *cupsCore) error { return err } - description, manufacturer, model := translatePPD(w.String()) + description, manufacturer, model, duplexMap := translatePPD(w.String(), vendorPPDOptions) if description == nil || manufacturer == "" || model == "" { return errors.New("Failed to parse PPD") } @@ -184,6 +188,7 @@ func (pce *ppdCacheEntry) refresh(cc *cupsCore) error { pce.description = *description pce.manufacturer = manufacturer pce.model = model + pce.duplexMap = duplexMap return nil } diff --git a/cups/translate-attrs.go b/cups/translate-attrs.go index 59b5055..396ad4e 100644 --- a/cups/translate-attrs.go +++ b/cups/translate-attrs.go @@ -4,7 +4,7 @@ // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd -// +build linux darwin +// +build linux darwin freebsd package cups @@ -15,8 +15,8 @@ import ( "strconv" "strings" - "github.com/google/cups-connector/cdd" - "github.com/google/cups-connector/log" + "github.com/google/cloud-print-connector/cdd" + "github.com/google/cloud-print-connector/log" ) // translateAttrs extracts a PrinterDescriptionSection, PrinterStateSection, name, default diplay name, UUID, and tags from maps of tags (CUPS attributes) @@ -67,6 +67,17 @@ func getUUID(printerTags map[string][]string) string { } func getState(printerTags map[string][]string) cdd.CloudDeviceStateType { + // Some CUPS backends (e.g. usb-darwin) add offline-report + // to printer-state-reasons when the printer is offline/disconnected + reasons, exists := printerTags[attrPrinterStateReasons] + if exists && len(reasons) > 0 { + for _, reason := range reasons { + if reason == "offline-report" { + return cdd.CloudDeviceStateStopped + } + } + } + if s, ok := printerTags[attrPrinterState]; ok { switch s[0] { case "3": @@ -434,17 +445,17 @@ func convertCopies(printerTags map[string][]string) *cdd.Copies { var colorByKeyword = map[string]cdd.ColorOption{ "auto": cdd.ColorOption{ - VendorID: fmt.Sprintf("%s%s%s", attrPrintColorMode, internalKeySeparator, "auto"), + VendorID: attrPrintColorMode + internalKeySeparator + "auto", Type: cdd.ColorTypeAuto, CustomDisplayNameLocalized: cdd.NewLocalizedString("Auto"), }, "color": cdd.ColorOption{ - VendorID: fmt.Sprintf("%s%s%s", attrPrintColorMode, internalKeySeparator, "color"), + VendorID: attrPrintColorMode + internalKeySeparator + "color", Type: cdd.ColorTypeStandardColor, CustomDisplayNameLocalized: cdd.NewLocalizedString("Color"), }, "monochrome": cdd.ColorOption{ - VendorID: fmt.Sprintf("%s%s%s", attrPrintColorMode, internalKeySeparator, "monochrome"), + VendorID: attrPrintColorMode + internalKeySeparator + "monochrome", Type: cdd.ColorTypeStandardMonochrome, CustomDisplayNameLocalized: cdd.NewLocalizedString("Monochrome"), }, @@ -456,18 +467,19 @@ func convertColorAttrs(printerTags map[string][]string) *cdd.Color { return nil } + c := cdd.Color{} + colorDefault, exists := printerTags[attrPrintColorModeDefault] if !exists || len(colorDefault) != 1 { colorDefault = colorSupported[:1] } - var c cdd.Color for _, color := range colorSupported { var co cdd.ColorOption var exists bool if co, exists = colorByKeyword[color]; !exists { co = cdd.ColorOption{ - VendorID: fmt.Sprintf("%s%s%s", attrPrintColorMode, internalKeySeparator, color), + VendorID: attrPrintColorMode + internalKeySeparator + color, Type: cdd.ColorTypeCustomColor, CustomDisplayNameLocalized: cdd.NewLocalizedString(color), } diff --git a/cups/translate-attrs_test.go b/cups/translate-attrs_test.go index 3781e54..a418fc6 100644 --- a/cups/translate-attrs_test.go +++ b/cups/translate-attrs_test.go @@ -4,7 +4,7 @@ // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd -// +build linux darwin +// +build linux darwin freebsd package cups @@ -13,8 +13,8 @@ import ( "reflect" "testing" - "github.com/google/cups-connector/cdd" - "github.com/google/cups-connector/log" + "github.com/google/cloud-print-connector/cdd" + "github.com/google/cloud-print-connector/log" ) func TestGetUUID(t *testing.T) { diff --git a/cups/translate-ppd.go b/cups/translate-ppd.go index 40c0e3c..e07db8c 100644 --- a/cups/translate-ppd.go +++ b/cups/translate-ppd.go @@ -4,7 +4,7 @@ // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd -// +build linux darwin +// +build linux darwin freebsd package cups @@ -14,12 +14,14 @@ import ( "strconv" "strings" - "github.com/google/cups-connector/cdd" - "github.com/google/cups-connector/log" + "github.com/google/cloud-print-connector/cdd" + "github.com/google/cloud-print-connector/lib" + "github.com/google/cloud-print-connector/log" ) const ( ppdBoolean = "Boolean" + ppdBRDuplex = "BRDuplex" ppdCMAndResolution = "CMAndResolution" ppdCloseGroup = "CloseGroup" ppdCloseSubGroup = "CloseSubGroup" @@ -88,6 +90,7 @@ var ( `(hpcups|hpijs|HPLIP),?\s+\d+(\.\d+)*|requires proprietary plugin` + `)\s*$`) rPageSize = regexp.MustCompile(`([\d.]+)(?:mm|in)?x([\d.]+)(mm|in)?`) + rPageSizeInPoints = regexp.MustCompile(`w([\d.]+)h([\d.]+)`) rColor = regexp.MustCompile(`(?i)^(?:cmy|rgb|color)`) rGray = regexp.MustCompile(`(?i)^(?:gray|black|mono)`) rCMAndResolutionPrefix = regexp.MustCompile(`(?i)^(?:on|off)\s*-?\s*`) @@ -123,49 +126,85 @@ type entry struct { options []statement } -// translatePPD extracts a PrinterDescriptionSection, manufacturer string, and model string +// translatePPD extracts a PrinterDescriptionSection, manufacturer string, model string, and DuplexVendorMap // from a PPD string. -func translatePPD(ppd string) (*cdd.PrinterDescriptionSection, string, string) { +func translatePPD(ppd string, vendorPPDOptions []string) (*cdd.PrinterDescriptionSection, string, string, lib.DuplexVendorMap) { statements := ppdToStatements(ppd) openUIStatements, installables, uiConstraints, standAlones := groupStatements(statements) openUIStatements = filterConstraints(openUIStatements, installables, uiConstraints) entriesByMainKeyword, entriesByTranslation := openUIStatementsToEntries(openUIStatements) + consideredMainKeywords := make(map[string]struct{}) + pds := cdd.PrinterDescriptionSection{ VendorCapability: &[]cdd.VendorCapability{}, } + + var duplexMap lib.DuplexVendorMap if e, exists := entriesByMainKeyword[ppdPageSize]; exists { pds.MediaSize = convertMediaSize(e) + consideredMainKeywords[e.mainKeyword] = struct{}{} } if e, exists := entriesByMainKeyword[ppdColorModel]; exists { pds.Color = convertColorPPD(e) + consideredMainKeywords[e.mainKeyword] = struct{}{} } else if e, exists := entriesByMainKeyword[ppdCMAndResolution]; exists { pds.Color = convertColorPPD(e) + consideredMainKeywords[e.mainKeyword] = struct{}{} } else if e, exists := entriesByMainKeyword[ppdSelectColor]; exists { pds.Color = convertColorPPD(e) + consideredMainKeywords[e.mainKeyword] = struct{}{} } if e, exists := entriesByMainKeyword[ppdDuplex]; exists { - pds.Duplex = convertDuplex(e) + pds.Duplex, duplexMap = convertDuplex(e) + consideredMainKeywords[e.mainKeyword] = struct{}{} + } else if e, exists := entriesByMainKeyword[ppdBRDuplex]; exists { + pds.Duplex, duplexMap = convertDuplex(e) + consideredMainKeywords[e.mainKeyword] = struct{}{} } else if e, exists := entriesByMainKeyword[ppdKMDuplex]; exists { - pds.Duplex = convertDuplex(e) + pds.Duplex, duplexMap = convertDuplex(e) + consideredMainKeywords[e.mainKeyword] = struct{}{} } if e, exists := entriesByMainKeyword[ppdResolution]; exists { pds.DPI = convertDPI(e) + consideredMainKeywords[e.mainKeyword] = struct{}{} } if e, exists := entriesByMainKeyword[ppdOutputBin]; exists { *pds.VendorCapability = append(*pds.VendorCapability, *convertVendorCapability(e)) + consideredMainKeywords[e.mainKeyword] = struct{}{} } if jobType, exists := entriesByMainKeyword[ppdJobType]; exists { if lockedPrintPassword, exists := entriesByMainKeyword[ppdLockedPrintPassword]; exists { vc := convertRicohLockedPrintPassword(jobType, lockedPrintPassword) if vc != nil { *pds.VendorCapability = append(*pds.VendorCapability, *vc) + consideredMainKeywords[jobType.mainKeyword] = struct{}{} + consideredMainKeywords[lockedPrintPassword.mainKeyword] = struct{}{} } } } if e, exists := entriesByTranslation[ppdPrintQualityTranslation]; exists { *pds.VendorCapability = append(*pds.VendorCapability, *convertVendorCapability(e)) + consideredMainKeywords[e.mainKeyword] = struct{}{} + } + + vendorPPDOptionsMap := make(map[string]struct{}, len(vendorPPDOptions)) + for _, o := range vendorPPDOptions { + vendorPPDOptionsMap[o] = struct{}{} + } + _, translateAllVendorPPDOptions := vendorPPDOptionsMap["all"] + for _, e := range entriesByMainKeyword { + if _, exists := consideredMainKeywords[e.mainKeyword]; exists { + continue + } + if !translateAllVendorPPDOptions { + if _, exists := vendorPPDOptionsMap[e.mainKeyword]; !exists { + continue + } + } + *pds.VendorCapability = append(*pds.VendorCapability, *convertVendorCapability(e)) } + if len(*pds.VendorCapability) == 0 { // Don't generate invalid CDD JSON. pds.VendorCapability = nil @@ -186,7 +225,7 @@ func translatePPD(ppd string) (*cdd.PrinterDescriptionSection, string, string) { } model = strings.TrimLeft(strings.TrimPrefix(model, manufacturer), " ") - return &pds, manufacturer, model + return &pds, manufacturer, model, duplexMap } // ppdToStatements converts a PPD file to a slice of statements. @@ -391,7 +430,7 @@ func convertMargins(hwMargins string) *cdd.Margins { if intValue > 0 { marginsType = cdd.MarginsStandard } - marginsMicrons[i-1] = pointsToMicrons(intValue) + marginsMicrons[i-1] = pointsToMicrons(float32(intValue)) } // HWResolution format: left, bottom, right, top. @@ -472,11 +511,11 @@ func convertColorPPD(e entry) *cdd.Color { } } - c := cdd.Color{VendorKey: e.mainKeyword} + c := cdd.Color{} if len(colorOptions) == 1 { colorName := cleanupColorName(colorOptions[0].optionKeyword, colorOptions[0].translation) co := cdd.ColorOption{ - VendorID: colorOptions[0].optionKeyword, + VendorID: e.mainKeyword + internalKeySeparator + colorOptions[0].optionKeyword, Type: cdd.ColorTypeStandardColor, CustomDisplayNameLocalized: cdd.NewLocalizedString(colorName), } @@ -485,7 +524,7 @@ func convertColorPPD(e entry) *cdd.Color { for _, o := range colorOptions { colorName := cleanupColorName(o.optionKeyword, o.translation) co := cdd.ColorOption{ - VendorID: o.optionKeyword, + VendorID: e.mainKeyword + internalKeySeparator + o.optionKeyword, Type: cdd.ColorTypeCustomColor, CustomDisplayNameLocalized: cdd.NewLocalizedString(colorName), } @@ -496,7 +535,7 @@ func convertColorPPD(e entry) *cdd.Color { if len(grayOptions) == 1 { colorName := cleanupColorName(grayOptions[0].optionKeyword, grayOptions[0].translation) co := cdd.ColorOption{ - VendorID: grayOptions[0].optionKeyword, + VendorID: e.mainKeyword + internalKeySeparator + grayOptions[0].optionKeyword, Type: cdd.ColorTypeStandardMonochrome, CustomDisplayNameLocalized: cdd.NewLocalizedString(colorName), } @@ -505,7 +544,7 @@ func convertColorPPD(e entry) *cdd.Color { for _, o := range grayOptions { colorName := cleanupColorName(o.optionKeyword, o.translation) co := cdd.ColorOption{ - VendorID: o.optionKeyword, + VendorID: e.mainKeyword + internalKeySeparator + o.optionKeyword, Type: cdd.ColorTypeCustomMonochrome, CustomDisplayNameLocalized: cdd.NewLocalizedString(colorName), } @@ -516,7 +555,7 @@ func convertColorPPD(e entry) *cdd.Color { for _, o := range otherOptions { colorName := cleanupColorName(o.optionKeyword, o.translation) co := cdd.ColorOption{ - VendorID: o.optionKeyword, + VendorID: e.mainKeyword + internalKeySeparator + o.optionKeyword, Type: cdd.ColorTypeCustomMonochrome, CustomDisplayNameLocalized: cdd.NewLocalizedString(colorName), } @@ -528,7 +567,7 @@ func convertColorPPD(e entry) *cdd.Color { } for i := range c.Option { - if c.Option[i].VendorID == e.defaultValue { + if c.Option[i].VendorID == e.mainKeyword+internalKeySeparator+e.defaultValue { c.Option[i].IsDefault = true return &c } @@ -538,41 +577,47 @@ func convertColorPPD(e entry) *cdd.Color { return &c } -func convertDuplex(e entry) *cdd.Duplex { - d := cdd.Duplex{VendorKey: e.mainKeyword} +func convertDuplex(e entry) (*cdd.Duplex, lib.DuplexVendorMap) { + d := cdd.Duplex{} + duplexMap := lib.DuplexVendorMap{} var foundDefault bool for _, o := range e.options { def := o.optionKeyword == e.defaultValue switch o.optionKeyword { case ppdNone, ppdFalse, ppdKMDuplexSingle: - d.Option = append(d.Option, cdd.DuplexOption{cdd.DuplexNoDuplex, def, o.optionKeyword}) + d.Option = append(d.Option, cdd.DuplexOption{cdd.DuplexNoDuplex, def}) + duplexMap[cdd.DuplexNoDuplex] = e.mainKeyword + internalKeySeparator + o.optionKeyword foundDefault = foundDefault || def case ppdDuplexNoTumble, ppdTrue, ppdKMDuplexDouble: - d.Option = append(d.Option, cdd.DuplexOption{cdd.DuplexLongEdge, def, o.optionKeyword}) + d.Option = append(d.Option, cdd.DuplexOption{cdd.DuplexLongEdge, def}) + duplexMap[cdd.DuplexLongEdge] = e.mainKeyword + internalKeySeparator + o.optionKeyword foundDefault = foundDefault || def case ppdDuplexTumble, ppdKMDuplexBooklet: - d.Option = append(d.Option, cdd.DuplexOption{cdd.DuplexShortEdge, def, o.optionKeyword}) + d.Option = append(d.Option, cdd.DuplexOption{cdd.DuplexShortEdge, def}) + duplexMap[cdd.DuplexShortEdge] = e.mainKeyword + internalKeySeparator + o.optionKeyword foundDefault = foundDefault || def default: if strings.HasPrefix(o.optionKeyword, "1") { - d.Option = append(d.Option, cdd.DuplexOption{cdd.DuplexNoDuplex, def, o.optionKeyword}) + d.Option = append(d.Option, cdd.DuplexOption{cdd.DuplexNoDuplex, def}) + duplexMap[cdd.DuplexNoDuplex] = e.mainKeyword + internalKeySeparator + o.optionKeyword foundDefault = foundDefault || def } else if strings.HasPrefix(o.optionKeyword, "2") { - d.Option = append(d.Option, cdd.DuplexOption{cdd.DuplexLongEdge, def, o.optionKeyword}) + d.Option = append(d.Option, cdd.DuplexOption{cdd.DuplexLongEdge, def}) + duplexMap[cdd.DuplexLongEdge] = e.mainKeyword + internalKeySeparator + o.optionKeyword foundDefault = foundDefault || def } } } if len(d.Option) == 0 { - return nil + return nil, nil } if !foundDefault { d.Option[0].IsDefault = true } - return &d + return &d, duplexMap } func convertDPI(e entry) *cdd.DPI { @@ -718,6 +763,13 @@ func getCustomMediaSizeOption(optionKeyword, translation string) *cdd.MediaSizeO found = rPageSize.FindStringSubmatch(translation) } if found == nil { + found = rPageSizeInPoints.FindStringSubmatch(optionKeyword) + if found == nil { + return nil + } + found = append(found, "points") + } + if len(found) != 4 { return nil } @@ -730,22 +782,21 @@ func getCustomMediaSizeOption(optionKeyword, translation string) *cdd.MediaSizeO return nil } - if found[3] == "mm" { - return &cdd.MediaSizeOption{ - Name: cdd.MediaSizeCustom, - WidthMicrons: mmToMicrons(float32(width)), - HeightMicrons: mmToMicrons(float32(height)), - VendorID: optionKeyword, - CustomDisplayNameLocalized: cdd.NewLocalizedString(translation), - } - } else { - return &cdd.MediaSizeOption{ - Name: cdd.MediaSizeCustom, - WidthMicrons: inchesToMicrons(float32(width)), - HeightMicrons: inchesToMicrons(float32(height)), - VendorID: optionKeyword, - CustomDisplayNameLocalized: cdd.NewLocalizedString(translation), - } + var toMicrons func(float32) int32 + switch found[3] { + case "mm": + toMicrons = mmToMicrons + case "points": + toMicrons = pointsToMicrons + default: + toMicrons = inchesToMicrons + } + return &cdd.MediaSizeOption{ + Name: cdd.MediaSizeCustom, + WidthMicrons: toMicrons(float32(width)), + HeightMicrons: toMicrons(float32(height)), + VendorID: optionKeyword, + CustomDisplayNameLocalized: cdd.NewLocalizedString(translation), } } @@ -757,8 +808,8 @@ func mmToMicrons(mm float32) int32 { return int32(mm*1000 + 0.5) } -func pointsToMicrons(points int64) int32 { - return int32(float32(points*25400)/72 + 0.5) +func pointsToMicrons(points float32) int32 { + return int32(points*25400/72 + 0.5) } var ppdMediaSizes = map[string]cdd.MediaSizeOption{ diff --git a/cups/translate-ppd_test.go b/cups/translate-ppd_test.go index 4b1968d..4681efe 100644 --- a/cups/translate-ppd_test.go +++ b/cups/translate-ppd_test.go @@ -4,7 +4,7 @@ // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd -// +build linux darwin +// +build linux darwin freebsd package cups @@ -13,14 +13,21 @@ import ( "reflect" "testing" - "github.com/google/cups-connector/cdd" + "github.com/google/cloud-print-connector/cdd" + "github.com/google/cloud-print-connector/lib" ) -func translationTest(t *testing.T, ppd string, expected *cdd.PrinterDescriptionSection) { - description, _, _ := translatePPD(ppd) - if !reflect.DeepEqual(expected, description) { +type testdata struct { + Pds *cdd.PrinterDescriptionSection + Dm lib.DuplexVendorMap +} + +func translationTest(t *testing.T, ppd string, vendorPPDOptions []string, expected testdata) { + description, _, _, dm := translatePPD(ppd, vendorPPDOptions) + actual := testdata{description, dm} + if !reflect.DeepEqual(expected, actual) { e, _ := json.Marshal(expected) - d, _ := json.Marshal(description) + d, _ := json.Marshal(actual) t.Logf("expected\n %s\ngot\n %s", e, d) t.Fail() } @@ -29,16 +36,19 @@ func translationTest(t *testing.T, ppd string, expected *cdd.PrinterDescriptionS func TestTrPrintingSpeed(t *testing.T) { ppd := `*PPD-Adobe: "4.3" *Throughput: "30"` - expected := &cdd.PrinterDescriptionSection{ - PrintingSpeed: &cdd.PrintingSpeed{ - []cdd.PrintingSpeedOption{ - cdd.PrintingSpeedOption{ - SpeedPPM: 30.0, + expected := testdata{ + &cdd.PrinterDescriptionSection{ + PrintingSpeed: &cdd.PrintingSpeed{ + []cdd.PrintingSpeedOption{ + cdd.PrintingSpeedOption{ + SpeedPPM: 30.0, + }, }, }, }, + nil, } - translationTest(t, ppd, expected) + translationTest(t, ppd, []string{}, expected) } func TestTrMediaSize(t *testing.T) { @@ -50,19 +60,24 @@ func TestTrMediaSize(t *testing.T) { *PageSize B5/B5 - JIS: "" *PageSize Letter/Letter: "" *PageSize HalfLetter/5.5x8.5: "" +*PageSize w81h252/Address - 1 1/8 x 3 1/2": "<>setpagedevice" *CloseUI: *PageSize` - expected := &cdd.PrinterDescriptionSection{ - MediaSize: &cdd.MediaSize{ - Option: []cdd.MediaSizeOption{ - cdd.MediaSizeOption{cdd.MediaSizeISOA3, mmToMicrons(297), mmToMicrons(420), false, false, "", "A3", cdd.NewLocalizedString("A3")}, - cdd.MediaSizeOption{cdd.MediaSizeISOB5, mmToMicrons(176), mmToMicrons(250), false, false, "", "ISOB5", cdd.NewLocalizedString("B5 (ISO)")}, - cdd.MediaSizeOption{cdd.MediaSizeJISB5, mmToMicrons(182), mmToMicrons(257), false, false, "", "B5", cdd.NewLocalizedString("B5 (JIS)")}, - cdd.MediaSizeOption{cdd.MediaSizeNALetter, inchesToMicrons(8.5), inchesToMicrons(11), false, true, "", "Letter", cdd.NewLocalizedString("Letter")}, - cdd.MediaSizeOption{cdd.MediaSizeCustom, inchesToMicrons(5.5), inchesToMicrons(8.5), false, false, "", "HalfLetter", cdd.NewLocalizedString("5.5x8.5")}, + expected := testdata{ + &cdd.PrinterDescriptionSection{ + MediaSize: &cdd.MediaSize{ + Option: []cdd.MediaSizeOption{ + cdd.MediaSizeOption{cdd.MediaSizeISOA3, mmToMicrons(297), mmToMicrons(420), false, false, "", "A3", cdd.NewLocalizedString("A3")}, + cdd.MediaSizeOption{cdd.MediaSizeISOB5, mmToMicrons(176), mmToMicrons(250), false, false, "", "ISOB5", cdd.NewLocalizedString("B5 (ISO)")}, + cdd.MediaSizeOption{cdd.MediaSizeJISB5, mmToMicrons(182), mmToMicrons(257), false, false, "", "B5", cdd.NewLocalizedString("B5 (JIS)")}, + cdd.MediaSizeOption{cdd.MediaSizeNALetter, inchesToMicrons(8.5), inchesToMicrons(11), false, true, "", "Letter", cdd.NewLocalizedString("Letter")}, + cdd.MediaSizeOption{cdd.MediaSizeCustom, inchesToMicrons(5.5), inchesToMicrons(8.5), false, false, "", "HalfLetter", cdd.NewLocalizedString("5.5x8.5")}, + cdd.MediaSizeOption{cdd.MediaSizeCustom, pointsToMicrons(81), pointsToMicrons(252), false, false, "", "w81h252", cdd.NewLocalizedString(`Address - 1 1/8 x 3 1/2"`)}, + }, }, }, + nil, } - translationTest(t, ppd, expected) + translationTest(t, ppd, []string{}, expected) } func TestTrColor(t *testing.T) { @@ -72,16 +87,18 @@ func TestTrColor(t *testing.T) { *ColorModel CMYK/Color: "(cmyk) RCsetdevicecolor" *ColorModel Gray/Black and White: "(gray) RCsetdevicecolor" *CloseUI: *ColorModel` - expected := &cdd.PrinterDescriptionSection{ - Color: &cdd.Color{ - Option: []cdd.ColorOption{ - cdd.ColorOption{"CMYK", cdd.ColorTypeStandardColor, "", false, cdd.NewLocalizedString("Color")}, - cdd.ColorOption{"Gray", cdd.ColorTypeStandardMonochrome, "", true, cdd.NewLocalizedString("Black and White")}, + expected := testdata{ + &cdd.PrinterDescriptionSection{ + Color: &cdd.Color{ + Option: []cdd.ColorOption{ + cdd.ColorOption{"ColorModel:CMYK", cdd.ColorTypeStandardColor, "", false, cdd.NewLocalizedString("Color")}, + cdd.ColorOption{"ColorModel:Gray", cdd.ColorTypeStandardMonochrome, "", true, cdd.NewLocalizedString("Black and White")}, + }, }, - VendorKey: "ColorModel", }, + nil, } - translationTest(t, ppd, expected) + translationTest(t, ppd, []string{}, expected) ppd = `*PPD-Adobe: "4.3" *OpenUI *CMAndResolution/Print Color as Gray: PickOne @@ -95,16 +112,18 @@ func TestTrColor(t *testing.T) { *End *CloseUI: *CMAndResolution ` - expected = &cdd.PrinterDescriptionSection{ - Color: &cdd.Color{ - Option: []cdd.ColorOption{ - cdd.ColorOption{"CMYKImageRET3600", cdd.ColorTypeStandardColor, "", true, cdd.NewLocalizedString("Color")}, - cdd.ColorOption{"Gray600x600dpi", cdd.ColorTypeStandardMonochrome, "", false, cdd.NewLocalizedString("Gray")}, + expected = testdata{ + &cdd.PrinterDescriptionSection{ + Color: &cdd.Color{ + Option: []cdd.ColorOption{ + cdd.ColorOption{"CMAndResolution:CMYKImageRET3600", cdd.ColorTypeStandardColor, "", true, cdd.NewLocalizedString("Color")}, + cdd.ColorOption{"CMAndResolution:Gray600x600dpi", cdd.ColorTypeStandardMonochrome, "", false, cdd.NewLocalizedString("Gray")}, + }, }, - VendorKey: "CMAndResolution", }, + nil, } - translationTest(t, ppd, expected) + translationTest(t, ppd, []string{}, expected) ppd = `*PPD-Adobe: "4.3" *OpenUI *CMAndResolution/Print Color as Gray: PickOne @@ -115,17 +134,19 @@ func TestTrColor(t *testing.T) { *CMAndResolution Gray600x600dpi/On - 600 dpi: "<> setpagedevice" *CloseUI: *CMAndResolution ` - expected = &cdd.PrinterDescriptionSection{ - Color: &cdd.Color{ - Option: []cdd.ColorOption{ - cdd.ColorOption{"CMYKImageRET2400", cdd.ColorTypeStandardColor, "", true, cdd.NewLocalizedString("Color, ImageRET 2400")}, - cdd.ColorOption{"Gray1200x1200dpi", cdd.ColorTypeCustomMonochrome, "", false, cdd.NewLocalizedString("Gray, ProRes 1200")}, - cdd.ColorOption{"Gray600x600dpi", cdd.ColorTypeCustomMonochrome, "", false, cdd.NewLocalizedString("Gray, 600 dpi")}, + expected = testdata{ + &cdd.PrinterDescriptionSection{ + Color: &cdd.Color{ + Option: []cdd.ColorOption{ + cdd.ColorOption{"CMAndResolution:CMYKImageRET2400", cdd.ColorTypeStandardColor, "", true, cdd.NewLocalizedString("Color, ImageRET 2400")}, + cdd.ColorOption{"CMAndResolution:Gray1200x1200dpi", cdd.ColorTypeCustomMonochrome, "", false, cdd.NewLocalizedString("Gray, ProRes 1200")}, + cdd.ColorOption{"CMAndResolution:Gray600x600dpi", cdd.ColorTypeCustomMonochrome, "", false, cdd.NewLocalizedString("Gray, 600 dpi")}, + }, }, - VendorKey: "CMAndResolution", }, + nil, } - translationTest(t, ppd, expected) + translationTest(t, ppd, []string{}, expected) ppd = `*PPD-Adobe: "4.3" *OpenUI *SelectColor/Select Color: PickOne @@ -135,16 +156,18 @@ func TestTrColor(t *testing.T) { *SelectColor Grayscale/Grayscale: "<> setpagedevice" *CloseUI: *SelectColor ` - expected = &cdd.PrinterDescriptionSection{ - Color: &cdd.Color{ - Option: []cdd.ColorOption{ - cdd.ColorOption{"Color", cdd.ColorTypeStandardColor, "", true, cdd.NewLocalizedString("Color")}, - cdd.ColorOption{"Grayscale", cdd.ColorTypeStandardMonochrome, "", false, cdd.NewLocalizedString("Grayscale")}, + expected = testdata{ + &cdd.PrinterDescriptionSection{ + Color: &cdd.Color{ + Option: []cdd.ColorOption{ + cdd.ColorOption{"SelectColor:Color", cdd.ColorTypeStandardColor, "", true, cdd.NewLocalizedString("Color")}, + cdd.ColorOption{"SelectColor:Grayscale", cdd.ColorTypeStandardMonochrome, "", false, cdd.NewLocalizedString("Grayscale")}, + }, }, - VendorKey: "SelectColor", }, + nil, } - translationTest(t, ppd, expected) + translationTest(t, ppd, []string{}, expected) } func TestTrDuplex(t *testing.T) { @@ -154,16 +177,21 @@ func TestTrDuplex(t *testing.T) { *Duplex None/Off: "" *Duplex DuplexNoTumble/Long Edge: "" *CloseUI: *Duplex` - expected := &cdd.PrinterDescriptionSection{ - Duplex: &cdd.Duplex{ - Option: []cdd.DuplexOption{ - cdd.DuplexOption{cdd.DuplexNoDuplex, true, "None"}, - cdd.DuplexOption{cdd.DuplexLongEdge, false, "DuplexNoTumble"}, + expected := testdata{ + &cdd.PrinterDescriptionSection{ + Duplex: &cdd.Duplex{ + Option: []cdd.DuplexOption{ + cdd.DuplexOption{cdd.DuplexNoDuplex, true}, + cdd.DuplexOption{cdd.DuplexLongEdge, false}, + }, }, - VendorKey: "Duplex", + }, + lib.DuplexVendorMap{ + cdd.DuplexNoDuplex: "Duplex:None", + cdd.DuplexLongEdge: "Duplex:DuplexNoTumble", }, } - translationTest(t, ppd, expected) + translationTest(t, ppd, []string{}, expected) } func TestTrKMDuplex(t *testing.T) { @@ -182,17 +210,23 @@ func TestTrKMDuplex(t *testing.T) { *End *CloseUI: *KMDuplex ` - expected := &cdd.PrinterDescriptionSection{ - Duplex: &cdd.Duplex{ - Option: []cdd.DuplexOption{ - cdd.DuplexOption{cdd.DuplexNoDuplex, false, "Single"}, - cdd.DuplexOption{cdd.DuplexLongEdge, true, "Double"}, - cdd.DuplexOption{cdd.DuplexShortEdge, false, "Booklet"}, + expected := testdata{ + &cdd.PrinterDescriptionSection{ + Duplex: &cdd.Duplex{ + Option: []cdd.DuplexOption{ + cdd.DuplexOption{cdd.DuplexNoDuplex, false}, + cdd.DuplexOption{cdd.DuplexLongEdge, true}, + cdd.DuplexOption{cdd.DuplexShortEdge, false}, + }, }, - VendorKey: "KMDuplex", + }, + lib.DuplexVendorMap{ + cdd.DuplexNoDuplex: "KMDuplex:Single", + cdd.DuplexLongEdge: "KMDuplex:Double", + cdd.DuplexShortEdge: "KMDuplex:Booklet", }, } - translationTest(t, ppd, expected) + translationTest(t, ppd, []string{}, expected) ppd = `*PPD-Adobe: "4.3" *OpenUI *KMDuplex/Duplex: Boolean @@ -202,16 +236,51 @@ func TestTrKMDuplex(t *testing.T) { *KMDuplex True/On: "<< /Duplex true >> setpagedevice" *CloseUI: *KMDuplex ` - expected = &cdd.PrinterDescriptionSection{ - Duplex: &cdd.Duplex{ - Option: []cdd.DuplexOption{ - cdd.DuplexOption{cdd.DuplexNoDuplex, true, "False"}, - cdd.DuplexOption{cdd.DuplexLongEdge, false, "True"}, + expected = testdata{ + &cdd.PrinterDescriptionSection{ + Duplex: &cdd.Duplex{ + Option: []cdd.DuplexOption{ + cdd.DuplexOption{cdd.DuplexNoDuplex, true}, + cdd.DuplexOption{cdd.DuplexLongEdge, false}, + }, }, - VendorKey: "KMDuplex", + }, + lib.DuplexVendorMap{ + cdd.DuplexNoDuplex: "KMDuplex:False", + cdd.DuplexLongEdge: "KMDuplex:True", }, } - translationTest(t, ppd, expected) + translationTest(t, ppd, []string{}, expected) +} + +func TestTrBRDuplex(t *testing.T) { + ppd := `*PPD-Adobe: "4.3" +*OpenUI *BRDuplex/Two-Sided Printing: PickOne +*OrderDependency: 25 AnySetup *BRDuplex +*DefaultBRDuplex: None +*BRDuplex DuplexTumble/DuplexTumble: " " +*BRDuplex DuplexNoTumble/DuplexNoTumble: " " +*BRDuplex None/OFF: " " +*CloseUI: *BRDuplex +` + + expected := testdata{ + &cdd.PrinterDescriptionSection{ + Duplex: &cdd.Duplex{ + Option: []cdd.DuplexOption{ + cdd.DuplexOption{cdd.DuplexShortEdge, false}, + cdd.DuplexOption{cdd.DuplexLongEdge, false}, + cdd.DuplexOption{cdd.DuplexNoDuplex, true}, + }, + }, + }, + lib.DuplexVendorMap{ + cdd.DuplexNoDuplex: "BRDuplex:None", + cdd.DuplexLongEdge: "BRDuplex:DuplexNoTumble", + cdd.DuplexShortEdge: "BRDuplex:DuplexTumble", + }, + } + translationTest(t, ppd, []string{}, expected) } func TestTrDPI(t *testing.T) { @@ -222,16 +291,19 @@ func TestTrDPI(t *testing.T) { *Resolution 1200x600dpi/1200x600 dpi: "" *Resolution 1200x1200dpi/1200 dpi: "" *CloseUI: *Resolution` - expected := &cdd.PrinterDescriptionSection{ - DPI: &cdd.DPI{ - Option: []cdd.DPIOption{ - cdd.DPIOption{600, 600, true, "", "600dpi", cdd.NewLocalizedString("600 dpi")}, - cdd.DPIOption{1200, 600, false, "", "1200x600dpi", cdd.NewLocalizedString("1200x600 dpi")}, - cdd.DPIOption{1200, 1200, false, "", "1200x1200dpi", cdd.NewLocalizedString("1200 dpi")}, + expected := testdata{ + &cdd.PrinterDescriptionSection{ + DPI: &cdd.DPI{ + Option: []cdd.DPIOption{ + cdd.DPIOption{600, 600, true, "", "600dpi", cdd.NewLocalizedString("600 dpi")}, + cdd.DPIOption{1200, 600, false, "", "1200x600dpi", cdd.NewLocalizedString("1200x600 dpi")}, + cdd.DPIOption{1200, 1200, false, "", "1200x1200dpi", cdd.NewLocalizedString("1200 dpi")}, + }, }, }, + nil, } - translationTest(t, ppd, expected) + translationTest(t, ppd, []string{}, expected) } func TestTrInputSlot(t *testing.T) { @@ -243,23 +315,26 @@ func TestTrInputSlot(t *testing.T) { *OutputBin Bin1/Internal Tray 2: "" *OutputBin External/External Tray: "" *CloseUI: *OutputBin` - expected := &cdd.PrinterDescriptionSection{ - VendorCapability: &[]cdd.VendorCapability{ - cdd.VendorCapability{ - ID: "OutputBin", - Type: cdd.VendorCapabilitySelect, - DisplayNameLocalized: cdd.NewLocalizedString("Destination"), - SelectCap: &cdd.SelectCapability{ - Option: []cdd.SelectCapabilityOption{ - cdd.SelectCapabilityOption{"Standard", "", true, cdd.NewLocalizedString("Internal Tray 1")}, - cdd.SelectCapabilityOption{"Bin1", "", false, cdd.NewLocalizedString("Internal Tray 2")}, - cdd.SelectCapabilityOption{"External", "", false, cdd.NewLocalizedString("External Tray")}, + expected := testdata{ + &cdd.PrinterDescriptionSection{ + VendorCapability: &[]cdd.VendorCapability{ + cdd.VendorCapability{ + ID: "OutputBin", + Type: cdd.VendorCapabilitySelect, + DisplayNameLocalized: cdd.NewLocalizedString("Destination"), + SelectCap: &cdd.SelectCapability{ + Option: []cdd.SelectCapabilityOption{ + cdd.SelectCapabilityOption{"Standard", "", true, cdd.NewLocalizedString("Internal Tray 1")}, + cdd.SelectCapabilityOption{"Bin1", "", false, cdd.NewLocalizedString("Internal Tray 2")}, + cdd.SelectCapabilityOption{"External", "", false, cdd.NewLocalizedString("External Tray")}, + }, }, }, }, }, + nil, } - translationTest(t, ppd, expected) + translationTest(t, ppd, []string{}, expected) } func TestTrPrintQuality(t *testing.T) { @@ -270,23 +345,26 @@ func TestTrPrintQuality(t *testing.T) { *HPPrintQuality 600dpi/600 dpi: "" *HPPrintQuality ProRes1200/ProRes 1200: "" *CloseUI: *HPPrintQuality` - expected := &cdd.PrinterDescriptionSection{ - VendorCapability: &[]cdd.VendorCapability{ - cdd.VendorCapability{ - ID: "HPPrintQuality", - Type: cdd.VendorCapabilitySelect, - DisplayNameLocalized: cdd.NewLocalizedString("Print Quality"), - SelectCap: &cdd.SelectCapability{ - Option: []cdd.SelectCapabilityOption{ - cdd.SelectCapabilityOption{"FastRes1200", "", true, cdd.NewLocalizedString("FastRes 1200")}, - cdd.SelectCapabilityOption{"600dpi", "", false, cdd.NewLocalizedString("600 dpi")}, - cdd.SelectCapabilityOption{"ProRes1200", "", false, cdd.NewLocalizedString("ProRes 1200")}, + expected := testdata{ + &cdd.PrinterDescriptionSection{ + VendorCapability: &[]cdd.VendorCapability{ + cdd.VendorCapability{ + ID: "HPPrintQuality", + Type: cdd.VendorCapabilitySelect, + DisplayNameLocalized: cdd.NewLocalizedString("Print Quality"), + SelectCap: &cdd.SelectCapability{ + Option: []cdd.SelectCapabilityOption{ + cdd.SelectCapabilityOption{"FastRes1200", "", true, cdd.NewLocalizedString("FastRes 1200")}, + cdd.SelectCapabilityOption{"600dpi", "", false, cdd.NewLocalizedString("600 dpi")}, + cdd.SelectCapabilityOption{"ProRes1200", "", false, cdd.NewLocalizedString("ProRes 1200")}, + }, }, }, }, }, + nil, } - translationTest(t, ppd, expected) + translationTest(t, ppd, []string{}, expected) } func TestRicohLockedPrint(t *testing.T) { @@ -316,19 +394,22 @@ func TestRicohLockedPrint(t *testing.T) { *CustomLockedPrintPassword True/Custom Password: "" *ParamCustomLockedPrintPassword Password: 1 passcode 4 8 ` - expected := &cdd.PrinterDescriptionSection{ - VendorCapability: &[]cdd.VendorCapability{ - cdd.VendorCapability{ - ID: "JobType:LockedPrint/LockedPrintPassword", - Type: cdd.VendorCapabilityTypedValue, - DisplayNameLocalized: cdd.NewLocalizedString("Password (4 numbers)"), - TypedValueCap: &cdd.TypedValueCapability{ - ValueType: cdd.TypedValueCapabilityTypeString, + expected := testdata{ + &cdd.PrinterDescriptionSection{ + VendorCapability: &[]cdd.VendorCapability{ + cdd.VendorCapability{ + ID: "JobType:LockedPrint/LockedPrintPassword", + Type: cdd.VendorCapabilityTypedValue, + DisplayNameLocalized: cdd.NewLocalizedString("Password (4 numbers)"), + TypedValueCap: &cdd.TypedValueCapability{ + ValueType: cdd.TypedValueCapabilityTypeString, + }, }, }, }, + nil, } - translationTest(t, ppd, expected) + translationTest(t, ppd, []string{}, expected) } func easyModelTest(t *testing.T, input, expected string) { @@ -365,3 +446,44 @@ func TestCleanupModel(t *testing.T) { easyModelTest(t, "LaserJet 4250 pcl3, hpcups 3.13.9", "LaserJet 4250") easyModelTest(t, "DesignJet T790 pcl, 1.0", "DesignJet T790") } + +func TestTrVendorPPDOptions(t *testing.T) { + ppd := `*PPD-Adobe: "4.3" +*OpenUI *CustomKey/Custom Translation: PickOne +*DefaultCustomKey: Some +*CustomKey None/Off: "" +*CustomKey Some/On: "" +*CustomKey Yes/Petunia: "" +*CloseUI: *CustomKey` + + expected := testdata{ + &cdd.PrinterDescriptionSection{ + VendorCapability: &[]cdd.VendorCapability{ + cdd.VendorCapability{ + ID: "CustomKey", + Type: cdd.VendorCapabilitySelect, + DisplayNameLocalized: cdd.NewLocalizedString("Custom Translation"), + SelectCap: &cdd.SelectCapability{ + Option: []cdd.SelectCapabilityOption{ + cdd.SelectCapabilityOption{"None", "", false, cdd.NewLocalizedString("Off")}, + cdd.SelectCapabilityOption{"Some", "", true, cdd.NewLocalizedString("On")}, + cdd.SelectCapabilityOption{"Yes", "", false, cdd.NewLocalizedString("Petunia")}, + }, + }, + }, + }, + }, + nil, + } + translationTest(t, ppd, []string{"CustomKey", "AnyOtherKey"}, expected) + translationTest(t, ppd, []string{"all"}, expected) + translationTest(t, ppd, []string{"CustomKey", "all"}, expected) + translationTest(t, ppd, []string{"AnyOtherKey", "all"}, expected) + + expected = testdata{ + &cdd.PrinterDescriptionSection{}, + nil, + } + translationTest(t, ppd, []string{}, expected) + translationTest(t, ppd, []string{"AnyOtherKey"}, expected) +} diff --git a/cups/translate-ticket.go b/cups/translate-ticket.go index 6e7e696..8e37e08 100644 --- a/cups/translate-ticket.go +++ b/cups/translate-ticket.go @@ -4,7 +4,7 @@ // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd -// +build linux darwin +// +build linux darwin freebsd package cups @@ -15,8 +15,8 @@ import ( "strconv" "strings" - "github.com/google/cups-connector/cdd" - "github.com/google/cups-connector/lib" + "github.com/google/cloud-print-connector/cdd" + "github.com/google/cloud-print-connector/lib" ) var rVendorIDKeyValue = regexp.MustCompile( @@ -31,8 +31,12 @@ func translateTicket(printer *lib.Printer, ticket *cdd.CloudJobTicket) (map[stri m := map[string]string{} for _, vti := range ticket.Print.VendorTicketItem { if vti.ID == ricohPasswordVendorID { + if vti.Value == "" { + // do not add specific map of options for Ricoh vendor like ppdLockedPrintPassword or ppdJobType when password is empty + continue; + } if !rRicohPasswordFormat.MatchString(vti.Value) { - return map[string]string{}, errors.New("Invalid password format") + return map[string]string{}, errors.New("Invalid password format") } } @@ -48,22 +52,28 @@ func translateTicket(printer *lib.Printer, ticket *cdd.CloudJobTicket) (map[stri } } if ticket.Print.Color != nil && printer.Description.Color != nil { + var colorString string if ticket.Print.Color.VendorID != "" { - m[printer.Description.Color.VendorKey] = ticket.Print.Color.VendorID + colorString = ticket.Print.Color.VendorID } else { - // The ticket doesn't provide the VendorID. Let's find it. + // The ticket doesn't provide the VendorID. Let's find it by Type. for _, colorOption := range printer.Description.Color.Option { if ticket.Print.Color.Type == colorOption.Type { - m[printer.Description.Color.VendorKey] = colorOption.VendorID + colorString = colorOption.VendorID + break } } } + parts := rVendorIDKeyValue.FindStringSubmatch(colorString) + if parts != nil && parts[2] != "" { + m[parts[1]] = parts[2] + } } if ticket.Print.Duplex != nil && printer.Description.Duplex != nil { - for _, duplexOption := range printer.Description.Duplex.Option { - if ticket.Print.Duplex.Type == duplexOption.Type { - m[printer.Description.Duplex.VendorKey] = duplexOption.VendorID - } + duplexString := printer.DuplexMap[ticket.Print.Duplex.Type] + parts := rVendorIDKeyValue.FindStringSubmatch(duplexString) + if parts != nil && parts[2] != "" { + m[parts[1]] = parts[2] } } if ticket.Print.PageOrientation != nil && printer.Description.PageOrientation != nil { diff --git a/cups/translate-ticket_test.go b/cups/translate-ticket_test.go index 6afebcf..a9741db 100644 --- a/cups/translate-ticket_test.go +++ b/cups/translate-ticket_test.go @@ -4,7 +4,7 @@ // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd -// +build linux darwin +// +build linux darwin freebsd package cups @@ -15,8 +15,8 @@ import ( "strings" "testing" - "github.com/google/cups-connector/cdd" - "github.com/google/cups-connector/lib" + "github.com/google/cloud-print-connector/cdd" + "github.com/google/cloud-print-connector/lib" ) func TestTranslateTicket(t *testing.T) { @@ -48,20 +48,17 @@ func TestTranslateTicket(t *testing.T) { Color: &cdd.Color{ Option: []cdd.ColorOption{ cdd.ColorOption{ - VendorID: "zebra-stripes", + VendorID: "ColorModel:zebra-stripes", Type: cdd.ColorTypeCustomMonochrome, }, }, - VendorKey: "ColorModel", }, Duplex: &cdd.Duplex{ Option: []cdd.DuplexOption{ cdd.DuplexOption{ - Type: cdd.DuplexNoDuplex, - VendorID: "None", + Type: cdd.DuplexNoDuplex, }, }, - VendorKey: "Duplex", }, PageOrientation: &cdd.PageOrientation{}, Copies: &cdd.Copies{}, @@ -80,13 +77,16 @@ func TestTranslateTicket(t *testing.T) { Collate: &cdd.Collate{}, ReverseOrder: &cdd.ReverseOrder{}, }, + DuplexMap: lib.DuplexVendorMap{ + cdd.DuplexNoDuplex: "Duplex:None", + }, } ticket.Print = cdd.PrintTicketSection{ VendorTicketItem: []cdd.VendorTicketItem{ cdd.VendorTicketItem{"number-up", "a"}, cdd.VendorTicketItem{"a:b/c:d/e", "f"}, }, - Color: &cdd.ColorTicketItem{VendorID: "zebra-stripes", Type: cdd.ColorTypeCustomMonochrome}, + Color: &cdd.ColorTicketItem{VendorID: "ColorModel:zebra-stripes", Type: cdd.ColorTypeCustomMonochrome}, Duplex: &cdd.DuplexTicketItem{Type: cdd.DuplexNoDuplex}, PageOrientation: &cdd.PageOrientationTicketItem{Type: cdd.PageOrientationAuto}, Copies: &cdd.CopiesTicketItem{Copies: 2}, @@ -139,20 +139,17 @@ func TestTranslateTicket(t *testing.T) { Color: &cdd.Color{ Option: []cdd.ColorOption{ cdd.ColorOption{ - VendorID: "color", + VendorID: "print-color-mode:color", Type: cdd.ColorTypeStandardColor, }, }, - VendorKey: "print-color-mode", }, Duplex: &cdd.Duplex{ Option: []cdd.DuplexOption{ cdd.DuplexOption{ - Type: cdd.DuplexLongEdge, - VendorID: "Single", + Type: cdd.DuplexLongEdge, }, }, - VendorKey: "KMDuplex", }, PageOrientation: &cdd.PageOrientation{}, DPI: &cdd.DPI{ @@ -166,8 +163,11 @@ func TestTranslateTicket(t *testing.T) { }, MediaSize: &cdd.MediaSize{}, } + printer.DuplexMap = lib.DuplexVendorMap{ + cdd.DuplexLongEdge: "KMDuplex:Single", + } ticket.Print = cdd.PrintTicketSection{ - Color: &cdd.ColorTicketItem{VendorID: "color", Type: cdd.ColorTypeStandardColor}, + Color: &cdd.ColorTicketItem{VendorID: "print-color-mode:color", Type: cdd.ColorTypeStandardColor}, Duplex: &cdd.DuplexTicketItem{Type: cdd.DuplexLongEdge}, PageOrientation: &cdd.PageOrientationTicketItem{Type: cdd.PageOrientationLandscape}, DPI: &cdd.DPITicketItem{100, 100, ""}, @@ -193,14 +193,13 @@ func TestTranslateTicket(t *testing.T) { printer.Description.Color = &cdd.Color{ Option: []cdd.ColorOption{ cdd.ColorOption{ - VendorID: "Gray600x600dpi", + VendorID: "CMAndResolution:Gray600x600dpi", Type: cdd.ColorTypeStandardColor, }, }, - VendorKey: "CMAndResolution", } ticket.Print = cdd.PrintTicketSection{ - Color: &cdd.ColorTicketItem{VendorID: "Gray600x600dpi", Type: cdd.ColorTypeStandardColor}, + Color: &cdd.ColorTicketItem{VendorID: "CMAndResolution:Gray600x600dpi", Type: cdd.ColorTypeStandardColor}, } expected = map[string]string{ "CMAndResolution": "Gray600x600dpi", @@ -218,14 +217,13 @@ func TestTranslateTicket(t *testing.T) { printer.Description.Color = &cdd.Color{ Option: []cdd.ColorOption{ cdd.ColorOption{ - VendorID: "Color", + VendorID: "SelectColor:Color", Type: cdd.ColorTypeStandardColor, }, }, - VendorKey: "SelectColor", } ticket.Print = cdd.PrintTicketSection{ - Color: &cdd.ColorTicketItem{VendorID: "Color"}, + Color: &cdd.ColorTicketItem{VendorID: "SelectColor:Color"}, } expected = map[string]string{ "SelectColor": "Color", @@ -283,10 +281,10 @@ func TestTranslateTicket_RicohLockedPrint(t *testing.T) { } expected = map[string]string{} o, err = translateTicket(&printer, &ticket) - if err == nil { - t.Log("expected error") + if err != nil { + t.Logf("did not expect error %s", err) t.Fail() - } + } if !reflect.DeepEqual(o, expected) { t.Logf("expected\n %+v\ngot\n %+v", expected, o) t.Fail() diff --git a/fcm/fcm.go b/fcm/fcm.go new file mode 100644 index 0000000..e3769da --- /dev/null +++ b/fcm/fcm.go @@ -0,0 +1,243 @@ +package fcm + +import ( + "bufio" + "encoding/json" + "fmt" + "net/http" + "strconv" + "time" +) + +import ( + "github.com/google/cloud-print-connector/log" + "github.com/google/cloud-print-connector/notification" +) + +const ( + gcpFcmSubscribePath = "fcm/subscribe" +) + +type FCM struct { + fcmServerBindURL string + cachedToken string + tokenRefreshTime time.Time + clientID string + proxyName string + fcmTTLSecs float64 + FcmSubscribe func(string) (interface{}, error) + + notifications chan<- notification.PrinterNotification + dead chan struct{} + + quit chan struct{} + backoff backoff +} + +type FCMMessage []struct { + From string `json:"from"` + Category string `json:"category"` + CollapseKey string `json:"collapse_key"` + Data struct { + Notification string `json:"notification"` + Subtype string `json:"subtype"` + } `json:"data"` + MessageID string `json:"message_id"` + TimeToLive int `json:"time_to_live"` +} + +func NewFCM(clientID string, proxyName string, fcmServerBindURL string, FcmSubscribe func(string) (interface{}, error), notifications chan<- notification.PrinterNotification) (*FCM, error) { + f := FCM{ + fcmServerBindURL, + "", + time.Time{}, + clientID, + proxyName, + 0, + FcmSubscribe, + notifications, + make(chan struct{}), + make(chan struct{}), + backoff{0, time.Second * 5, time.Minute * 5}, + } + return &f, nil +} + +// get token from GCP and connect to FCM. +func (f *FCM) Init() { + iidToken := f.GetTokenWithRetry() + if err := f.ConnectToFcm(f.notifications, iidToken, f.dead, f.quit); err != nil { + for err != nil { + f.backoff.addError() + log.Errorf("FCM restart failed, will try again in %4.0f s: %s", + f.backoff.delay().Seconds(), err) + time.Sleep(f.backoff.delay()) + err = f.ConnectToFcm(f.notifications, iidToken, f.dead, f.quit) + } + f.backoff.reset() + log.Info("FCM conversation restarted successfully") + } + + go f.KeepFcmAlive() +} + +// Quit terminates the FCM conversation so that new jobs stop arriving. +func (f *FCM) Quit() { + // Signal to KeepFCMAlive. + close(f.quit) +} + +// Fcm notification listener +func (f *FCM) ConnectToFcm(fcmNotifications chan<- notification.PrinterNotification, iidToken string, dead chan<- struct{}, quit chan<- struct{}) error { + log.Debugf("Connecting to %s?token=%s", f.fcmServerBindURL, iidToken) + resp, err := http.Get(fmt.Sprintf("%s?token=%s", f.fcmServerBindURL, iidToken)) + if err != nil { + // retrying exponentially + log.Errorf("%v", err) + return err + } + if resp.StatusCode == 200 { + reader := bufio.NewReader(resp.Body) + go func() { + for { + printerId, err := GetPrinterID(reader) + if len(printerId) > 0 { + pn := notification.PrinterNotification{printerId, notification.PrinterNewJobs} + fcmNotifications <- pn + } + if err != nil { + log.Info("DRAIN message received, client reconnecting.") + dead <- struct{}{} + break + } + } + }() + } + return nil +} + +func GetPrinterID(reader *bufio.Reader) (string, error) { + raw_input, err := reader.ReadBytes('\n') + if err == nil { + // Trim last \n char + raw_input = raw_input[:len(raw_input) - 1] + buffer_size, _ := strconv.Atoi(string(raw_input)) + notification_buffer := make([]byte, buffer_size) + var sofar, sz int + for err == nil && sofar < buffer_size { + sz, err = reader.Read(notification_buffer[sofar:]) + sofar += sz + } + + if sofar == buffer_size { + var d [][]interface{} + var f FCMMessage + json.Unmarshal([]byte(string(notification_buffer)), &d) + s, _ := json.Marshal(d[0][1]) + json.Unmarshal(s, &f) + return f[0].Data.Notification, err + } + } + return "", err +} + +type backoff struct { + // The number of consecutive connection errors. + numErrors uint + // The minimum amount of time to backoff. + minBackoff time.Duration + // The maximum amount of time to backoff. + maxBackoff time.Duration +} + +// Computes the amount of time to delay based on the number of errors. +func (b *backoff) delay() time.Duration { + if b.numErrors == 0 { + // Never delay when there are no errors. + return 0 + } + curDelay := b.minBackoff + for i := uint(1); i < b.numErrors; i++ { + curDelay = curDelay * 2 + } + if curDelay > b.maxBackoff { + return b.maxBackoff + } + return curDelay +} + +// Adds an observed error to inform the backoff delay decision. +func (b *backoff) addError() { + log.Info("err count") + b.numErrors++ +} + +// Resets the backoff back to having no errors. +func (b *backoff) reset() { + b.numErrors = 0 +} + +// Restart FCM connection when lost. +func (f *FCM) KeepFcmAlive() { + for { + select { + case <-f.dead: + iidToken := f.GetTokenWithRetry() + log.Error("FCM conversation died; restarting") + if err := f.ConnectToFcm(f.notifications, iidToken, f.dead, f.quit); err != nil { + for err != nil { + f.backoff.addError() + log.Errorf("FCM connection restart failed, will try again in %4.0f s: %s", + f.backoff.delay().Seconds(), err) + time.Sleep(f.backoff.delay()) + err = f.ConnectToFcm(f.notifications, iidToken, f.dead, f.quit) + } + f.backoff.reset() + log.Info("FCM conversation restarted successfully") + } + + case <-f.quit: + log.Info("Fcm client Quitting ...") + // quitting keeping alive + return + } + } +} + +func (f *FCM) GetTokenWithRetry() string { + retryCount := 3 + iidToken, err := f.GetToken() + for err != nil && retryCount < 3 { + retryCount -= 1 + log.Errorf("unable to get FCM token from GCP server, will try again in 10s: %s", err) + time.Sleep(10 * time.Second) + iidToken, err = f.GetToken() + } + if err != nil { + log.Errorf("unable to get FCM token from GCP server.") + panic(err) + } + return iidToken +} + +// Returns cached token and Refresh token if needed. +func (f *FCM) GetToken() (string, error) { + if f.tokenRefreshTime == (time.Time{}) || time.Now().UTC().Sub(f.tokenRefreshTime).Seconds() > f.fcmTTLSecs { + result, err := f.FcmSubscribe(fmt.Sprintf("%s?client=%s&proxy=%s", gcpFcmSubscribePath, f.clientID, f.proxyName)) + if err != nil { + log.Errorf("Unable to subscribe to FCM : %s", err) + return "", err + } + token := result.(map[string]interface{})["token"] + ttlSeconds, err := strconv.ParseFloat(result.(map[string]interface{})["fcmttl"].(string), 64) + if err != nil { + log.Errorf("Failed to parse FCM ttl : %s", err) + return "", err + } + f.fcmTTLSecs = ttlSeconds + log.Info("Updated FCM token.") + f.cachedToken = token.(string) + f.tokenRefreshTime = time.Now().UTC() + } + return f.cachedToken, nil +} \ No newline at end of file diff --git a/fcm/fcm_test.go b/fcm/fcm_test.go new file mode 100644 index 0000000..17ba9b1 --- /dev/null +++ b/fcm/fcm_test.go @@ -0,0 +1,67 @@ +package fcm_test + +import ( + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "testing" + "time" +) + +import ( + "github.com/google/cloud-print-connector/fcm" + "github.com/google/cloud-print-connector/notification" +) + +func TestFCM_ReceiveNotification(t *testing.T) { + + // test FCM server + handler := func(w http.ResponseWriter, r *http.Request) { + printerNotificationStr := + `148 +[[4,[{"from":"xyz","category":"js","collapse_key":"xyz","data":{"notification":"printerId","subtype":"xyz"},"message_id":"xyz","time_to_live":60}]]]` + fmt.Fprint(w, printerNotificationStr) + } + + ts := httptest.NewServer(http.HandlerFunc(handler)) + defer ts.Close() + + var f *fcm.FCM + notifications := make(chan notification.PrinterNotification, 5) + + // sample notification + var fcmToken map[string]interface{} + fcmTokenStr := `{"fcmttl":"2419200","request":{"params":{"client":["xyz"],"proxy":["xyz"]},"time":"0","user":"xyz","users":["xyz"]},"success":true,"token":"token","xsrf_token":"xyz"}` + json.Unmarshal([]byte(fcmTokenStr), &fcmToken) + + f, err := fcm.NewFCM("clientid", "", ts.URL, func(input string) (interface{}, error) { return fcmToken, nil }, notifications) + defer f.Quit() + if err != nil { + t.Fatal(err) + } + + // This method is stubbed. + result, err := f.FcmSubscribe("SubscribeUrl") + if err != nil { + t.Fatal(err) + } + + token := result.(map[string]interface{})["token"].(string) + dead := make(chan struct{}) + quit := make(chan struct{}) + + // bind to FCM to receive notifications + f.ConnectToFcm(notifications, token, dead, quit) + go func() { + time.Sleep(4 * time.Second) + // time out + notifications <- notification.PrinterNotification{"dummy", notification.PrinterNewJobs} + }() + message := <-notifications + + // verify if right message received. + if message.GCPID != "printerId" { + t.Fatal("Did not receive right printer notification") + } +} diff --git a/gcp-connector-util/gcp-cups-connector-util.go b/gcp-connector-util/gcp-cups-connector-util.go index 8df710c..7f52703 100644 --- a/gcp-connector-util/gcp-cups-connector-util.go +++ b/gcp-connector-util/gcp-cups-connector-util.go @@ -10,358 +10,287 @@ package main import ( "encoding/json" + "errors" "fmt" "io/ioutil" - "log" "strings" "sync" - "github.com/codegangsta/cli" - "github.com/google/cups-connector/cdd" - "github.com/google/cups-connector/gcp" - "github.com/google/cups-connector/lib" + "github.com/google/cloud-print-connector/cdd" + "github.com/google/cloud-print-connector/gcp" + "github.com/google/cloud-print-connector/lib" + "github.com/urfave/cli" ) -var commonCommands = []cli.Command{ - cli.Command{ +var commonCommands = []*cli.Command{ + &cli.Command{ Name: "delete-all-gcp-printers", Usage: "Delete all printers associated with this connector", Action: deleteAllGCPPrinters, }, - cli.Command{ - Name: "update-config-file", - Usage: "Add new options to config file after update", - Action: updateConfigFile, + &cli.Command{ + Name: "backfill-config-file", + Usage: "Add all keys, with default values, to the config file", + Action: backfillConfigFile, }, - cli.Command{ + &cli.Command{ + Name: "sparse-config-file", + Usage: "Remove all keys, with non-default values, from the config file", + Action: sparseConfigFile, + }, + &cli.Command{ Name: "delete-gcp-job", Usage: "Deletes one GCP job", Action: deleteGCPJob, Flags: []cli.Flag{ - cli.StringFlag{ + &cli.StringFlag{ Name: "job-id", }, }, }, - cli.Command{ + &cli.Command{ Name: "cancel-gcp-job", Usage: "Cancels one GCP job", Action: cancelGCPJob, Flags: []cli.Flag{ - cli.StringFlag{ + &cli.StringFlag{ Name: "job-id", }, }, }, - cli.Command{ + &cli.Command{ Name: "delete-all-gcp-printer-jobs", Usage: "Delete all queued jobs associated with a printer", Action: deleteAllGCPPrinterJobs, Flags: []cli.Flag{ - cli.StringFlag{ + &cli.StringFlag{ Name: "printer-id", }, }, }, - cli.Command{ + &cli.Command{ Name: "cancel-all-gcp-printer-jobs", Usage: "Cancels all queued jobs associated with a printer", Action: cancelAllGCPPrinterJobs, Flags: []cli.Flag{ - cli.StringFlag{ + &cli.StringFlag{ Name: "printer-id", }, }, }, - cli.Command{ + &cli.Command{ Name: "show-gcp-printer-status", - Usage: "Shows the current status of a printer and it's jobs", + Usage: "Shows the current status of a printer and its jobs", Action: showGCPPrinterStatus, Flags: []cli.Flag{ - cli.StringFlag{ + &cli.StringFlag{ Name: "printer-id", }, }, }, - cli.Command{ + &cli.Command{ Name: "share-gcp-printer", Usage: "Shares a printer with user or group", Action: shareGCPPrinter, Flags: []cli.Flag{ - cli.StringFlag{ + &cli.StringFlag{ Name: "printer-id", - Usage: "Printer to share.", + Usage: "Printer to share", }, - cli.StringFlag{ + &cli.StringFlag{ Name: "email", - Usage: "Group or user to share with.", + Usage: "Group or user to share with", }, - cli.StringFlag{ + &cli.StringFlag{ Name: "role", Value: "USER", - Usage: "Role granted. user or manager.", + Usage: "Role granted. user or manager", }, - cli.BoolTFlag{ + &cli.BoolFlag{ Name: "skip-notification", Usage: "Skip sending email notice. Defaults to true", + DefaultText: "1", }, - cli.BoolFlag{ + &cli.BoolFlag{ Name: "public", Usage: "Make the printer public (anyone can print)", }, }, }, - cli.Command{ + &cli.Command{ Name: "unshare-gcp-printer", - Usage: "Removes user or group access to printer.", + Usage: "Removes user or group access to printer", Action: unshareGCPPrinter, Flags: []cli.Flag{ - cli.StringFlag{ + &cli.StringFlag{ Name: "printer-id", - Usage: "Printer to unshare.", + Usage: "Printer to unshare", }, - cli.StringFlag{ + &cli.StringFlag{ Name: "email", - Usage: "Group or user to remove.", + Usage: "Group or user to remove", }, - cli.BoolFlag{ + &cli.BoolFlag{ Name: "public", - Usage: "Remove public printer access.", + Usage: "Remove public printer access", }, }, }, - cli.Command{ + &cli.Command{ Name: "update-gcp-printer", - Usage: "Modifies settings for a printer.", + Usage: "Modifies settings for a printer", Action: updateGCPPrinter, Flags: []cli.Flag{ - cli.StringFlag{ + &cli.StringFlag{ Name: "printer-id", - Usage: "Printer to update.", + Usage: "Printer to update", }, - cli.BoolFlag{ + &cli.BoolFlag{ Name: "enable-quota", - Usage: "Set a daily per-user quota.", + Usage: "Set a daily per-user quota", }, - cli.BoolFlag{ + &cli.BoolFlag{ Name: "disable-quota", - Usage: "Disable daily per-user quota.", + Usage: "Disable daily per-user quota", }, - cli.IntFlag{ + &cli.IntFlag{ Name: "daily-quota", - Usage: "Pages per-user per-day.", + Usage: "Pages per-user per-day", }, }, }, } // getConfig returns a config object -func getConfig(context *cli.Context) *lib.Config { +func getConfig(context *cli.Context) (*lib.Config, error) { config, _, err := lib.GetConfig(context) if err != nil { - log.Fatalln(err) + return nil, err } - return config + return config, nil } // getGCP returns a GoogleCloudPrint object -func getGCP(config *lib.Config) *gcp.GoogleCloudPrint { - gcp, err := gcp.NewGoogleCloudPrint(config.GCPBaseURL, config.RobotRefreshToken, +func getGCP(config *lib.Config) (*gcp.GoogleCloudPrint, error) { + return gcp.NewGoogleCloudPrint(config.GCPBaseURL, config.RobotRefreshToken, config.UserRefreshToken, config.ProxyName, config.GCPOAuthClientID, config.GCPOAuthClientSecret, config.GCPOAuthAuthURL, config.GCPOAuthTokenURL, - 0, nil) - if err != nil { - log.Fatalln(err) - } - return gcp + 0, nil, false) } -// commonUpdateConfig updates the config object, with the help of configMap, -// which can indicate the absence of a value. -// Returns true if config was changed. -// -// Each platform should define a function updateConfig(*lib.Config, map[string]interface{}) -// which may call this function. -func commonUpdateConfig(config *lib.Config, configMap map[string]interface{}) bool { - dirty := false - - if _, exists := configMap["xmpp_server"]; !exists { - dirty = true - fmt.Println("Added xmpp_server") - config.XMPPServer = lib.DefaultConfig.XMPPServer - } - if _, exists := configMap["xmpp_port"]; !exists { - dirty = true - fmt.Println("Added xmpp_port") - config.XMPPPort = lib.DefaultConfig.XMPPPort - } - if _, exists := configMap["gcp_xmpp_ping_timeout"]; !exists { - dirty = true - fmt.Println("Added gcp_xmpp_ping_timeout") - config.XMPPPingTimeout = lib.DefaultConfig.XMPPPingTimeout - } - if _, exists := configMap["gcp_xmpp_ping_interval_default"]; !exists { - dirty = true - fmt.Println("Added gcp_xmpp_ping_interval_default") - config.XMPPPingInterval = lib.DefaultConfig.XMPPPingInterval - } - if _, exists := configMap["gcp_base_url"]; !exists { - dirty = true - fmt.Println("Added gcp_base_url") - config.GCPBaseURL = lib.DefaultConfig.GCPBaseURL - } - if _, exists := configMap["gcp_oauth_client_id"]; !exists { - dirty = true - fmt.Println("Added gcp_oauth_client_id") - config.GCPOAuthClientID = lib.DefaultConfig.GCPOAuthClientID - } - if _, exists := configMap["gcp_oauth_client_secret"]; !exists { - dirty = true - fmt.Println("Added gcp_oauth_client_secret") - config.GCPOAuthClientSecret = lib.DefaultConfig.GCPOAuthClientSecret - } - if _, exists := configMap["gcp_oauth_auth_url"]; !exists { - dirty = true - fmt.Println("Added gcp_oauth_auth_url") - config.GCPOAuthAuthURL = lib.DefaultConfig.GCPOAuthAuthURL - } - if _, exists := configMap["gcp_oauth_token_url"]; !exists { - dirty = true - fmt.Println("Added gcp_oauth_token_url") - config.GCPOAuthTokenURL = lib.DefaultConfig.GCPOAuthTokenURL - } - if _, exists := configMap["gcp_max_concurrent_downloads"]; !exists { - dirty = true - fmt.Println("Added gcp_max_concurrent_downloads") - config.GCPMaxConcurrentDownloads = lib.DefaultConfig.GCPMaxConcurrentDownloads - } - if _, exists := configMap["cups_job_queue_size"]; !exists { - dirty = true - fmt.Println("Added cups_job_queue_size") - config.NativeJobQueueSize = lib.DefaultConfig.NativeJobQueueSize - } - if _, exists := configMap["cups_printer_poll_interval"]; !exists { - dirty = true - fmt.Println("Added cups_printer_poll_interval") - config.NativePrinterPollInterval = lib.DefaultConfig.NativePrinterPollInterval - } - if _, exists := configMap["prefix_job_id_to_job_title"]; !exists { - dirty = true - fmt.Println("Added prefix_job_id_to_job_title") - config.PrefixJobIDToJobTitle = lib.DefaultConfig.PrefixJobIDToJobTitle - } - if _, exists := configMap["display_name_prefix"]; !exists { - dirty = true - fmt.Println("Added display_name_prefix") - config.DisplayNamePrefix = lib.DefaultConfig.DisplayNamePrefix - } - if _, exists := configMap["printer_blacklist"]; !exists { - dirty = true - fmt.Println("Added printer_blacklist") - config.PrinterBlacklist = lib.DefaultConfig.PrinterBlacklist - } - if _, exists := configMap["local_printing_enable"]; !exists { - dirty = true - fmt.Println("Added local_printing_enable") - config.LocalPrintingEnable = lib.DefaultConfig.LocalPrintingEnable - } - if _, exists := configMap["cloud_printing_enable"]; !exists { - dirty = true - _, robot_token_exists := configMap["robot_refresh_token"] - fmt.Println("Added cloud_printing_enable") - if robot_token_exists { - config.CloudPrintingEnable = true - } else { - config.CloudPrintingEnable = lib.DefaultConfig.CloudPrintingEnable - } - } - if _, exists := configMap["log_level"]; !exists { - dirty = true - fmt.Println("Added log_level") - config.LogLevel = lib.DefaultConfig.LogLevel - } - - return dirty -} - -// updateConfigFile opens the config file, adds any missing fields, -// writes the config file back. -func updateConfigFile(context *cli.Context) { - config, configFilename, err := lib.GetConfig(context) +// backfillConfigFile opens the config file, adds all missing keys +// and default values, then writes the config file back. +func backfillConfigFile(context *cli.Context) error { + config, cfBefore, err := lib.GetConfig(context) if err != nil { - log.Fatalln(err) + return err } - if configFilename == "" { - fmt.Println("Could not find a config file to update") - return + if cfBefore == "" { + return fmt.Errorf("Could not find a config file to backfill") } // Same config in []byte format. - configRaw, err := ioutil.ReadFile(configFilename) + configRaw, err := ioutil.ReadFile(cfBefore) if err != nil { - log.Fatalln(err) + return err } // Same config in map format so that we can detect missing keys. var configMap map[string]interface{} if err = json.Unmarshal(configRaw, &configMap); err != nil { - log.Fatalln(err) + return err } - dirty := updateConfig(config, configMap) + if cfWritten, err := config.Backfill(configMap).ToFile(context); err != nil { + return fmt.Errorf("Failed to write config file: %s", err) + } else { + fmt.Printf("Wrote %s\n", cfWritten) + } + return nil +} + +// sparseConfigFile opens the config file, removes most keys +// that have default values, then writes the config file back. +func sparseConfigFile(context *cli.Context) error { + config, cfBefore, err := lib.GetConfig(context) + if err != nil { + return err + } + if cfBefore == "" { + return errors.New("Could not find a config file to sparse") + } - if dirty { - config.ToFile(context) - fmt.Printf("Wrote %s\n", configFilename) + if cfWritten, err := config.Sparse(context).ToFile(context); err != nil { + return fmt.Errorf("Failed to write config file: %s\n", err) } else { - fmt.Println("Nothing to update") + fmt.Printf("Wrote %s\n", cfWritten) } + return nil } // deleteAllGCPPrinters finds all GCP printers associated with this // connector, deletes them from GCP. -func deleteAllGCPPrinters(context *cli.Context) { - config := getConfig(context) - gcp := getGCP(config) +func deleteAllGCPPrinters(context *cli.Context) error { + config, err := getConfig(context) + if err != nil { + return err + } + gcp, err := getGCP(config) + if err != nil { + return err + } printers, err := gcp.List() if err != nil { - log.Fatalln(err) + return err } var wg sync.WaitGroup for gcpID, name := range printers { wg.Add(1) go func(gcpID, name string) { + defer wg.Done() err := gcp.Delete(gcpID) if err != nil { fmt.Printf("Failed to delete %s \"%s\": %s\n", gcpID, name, err) } else { fmt.Printf("Deleted %s \"%s\" from GCP\n", gcpID, name) } - wg.Done() }(gcpID, name) } wg.Wait() + return nil } // deleteGCPJob deletes one GCP job -func deleteGCPJob(context *cli.Context) { - config := getConfig(context) - gcp := getGCP(config) +func deleteGCPJob(context *cli.Context) error { + config, err := getConfig(context) + if err != nil { + return err + } + gcp, err := getGCP(config) + if err != nil { + return err + } - err := gcp.DeleteJob(context.String("job-id")) + err = gcp.DeleteJob(context.String("job-id")) if err != nil { - fmt.Printf("Failed to delete GCP job %s: %s\n", context.String("job-id"), err) - } else { - fmt.Printf("Deleted GCP job %s\n", context.String("job-id")) + return fmt.Errorf("Failed to delete GCP job %s: %s\n", context.String("job-id"), err) } + fmt.Printf("Deleted GCP job %s\n", context.String("job-id")) + return nil } // cancelGCPJob cancels one GCP job -func cancelGCPJob(context *cli.Context) { - config := getConfig(context) - gcp := getGCP(config) +func cancelGCPJob(context *cli.Context) error { + config, err := getConfig(context) + if err != nil { + return err + } + gcp, err := getGCP(config) + if err != nil { + return err + } cancelState := cdd.PrintJobStateDiff{ State: &cdd.JobState{ @@ -370,23 +299,29 @@ func cancelGCPJob(context *cli.Context) { }, } - err := gcp.Control(context.String("job-id"), &cancelState) + err = gcp.Control(context.String("job-id"), &cancelState) if err != nil { - fmt.Printf("Failed to cancel GCP job %s: %s\n", context.String("job-id"), err) - } else { - fmt.Printf("Canceled GCP job %s\n", context.String("job-id")) + return fmt.Errorf("Failed to cancel GCP job %s: %s", context.String("job-id"), err) } + fmt.Printf("Canceled GCP job %s\n", context.String("job-id")) + return nil } // deleteAllGCPPrinterJobs finds all GCP printer jobs associated with a // a given printer id and deletes them. -func deleteAllGCPPrinterJobs(context *cli.Context) { - config := getConfig(context) - gcp := getGCP(config) +func deleteAllGCPPrinterJobs(context *cli.Context) error { + config, err := getConfig(context) + if err != nil { + return err + } + gcp, err := getGCP(config) + if err != nil { + return err + } jobs, err := gcp.Fetch(context.String("printer-id")) if err != nil { - log.Fatalln(err) + return err } if len(jobs) == 0 { @@ -409,17 +344,24 @@ func deleteAllGCPPrinterJobs(context *cli.Context) { for _ = range jobs { <-ch } + return nil } // cancelAllGCPPrinterJobs finds all GCP printer jobs associated with a // a given printer id and cancels them. -func cancelAllGCPPrinterJobs(context *cli.Context) { - config := getConfig(context) - gcp := getGCP(config) +func cancelAllGCPPrinterJobs(context *cli.Context) error { + config, err := getConfig(context) + if err != nil { + return err + } + gcp, err := getGCP(config) + if err != nil { + return err + } jobs, err := gcp.Fetch(context.String("printer-id")) if err != nil { - log.Fatalln(err) + return err } if len(jobs) == 0 { @@ -449,16 +391,23 @@ func cancelAllGCPPrinterJobs(context *cli.Context) { for _ = range jobs { <-ch } + return nil } // showGCPPrinterStatus shows the current status of a GCP printer and it's jobs -func showGCPPrinterStatus(context *cli.Context) { - config := getConfig(context) - gcp := getGCP(config) +func showGCPPrinterStatus(context *cli.Context) error { + config, err := getConfig(context) + if err != nil { + return err + } + gcp, err := getGCP(config) + if err != nil { + return err + } printer, _, err := gcp.Printer(context.String("printer-id")) if err != nil { - log.Fatalln(err) + return err } fmt.Println("Name:", printer.DefaultDisplayName) @@ -466,7 +415,7 @@ func showGCPPrinterStatus(context *cli.Context) { jobs, err := gcp.Jobs(context.String("printer-id")) if err != nil { - log.Fatalln(err) + return err } // Only init common states. Unusual states like DRAFT will only be shown @@ -488,12 +437,19 @@ func showGCPPrinterStatus(context *cli.Context) { for state, count := range jobStateCounts { fmt.Println(" ", state, ":", count) } + return nil } // shareGCPPrinter shares a GCP printer -func shareGCPPrinter(context *cli.Context) { - config := getConfig(context) - gcpConn := getGCP(config) +func shareGCPPrinter(context *cli.Context) error { + config, err := getConfig(context) + if err != nil { + return err + } + gcpConn, err := getGCP(config) + if err != nil { + return err + } var role gcp.Role switch strings.ToUpper(context.String("role")) { @@ -502,11 +458,10 @@ func shareGCPPrinter(context *cli.Context) { case "MANAGER": role = gcp.Manager default: - fmt.Println("role should be user or manager.") - return + return fmt.Errorf("role should be user or manager.") } - err := gcpConn.Share(context.String("printer-id"), context.String("email"), + err = gcpConn.Share(context.String("printer-id"), context.String("email"), role, context.Bool("skip-notification"), context.Bool("public")) var sharedWith string if context.Bool("public") { @@ -515,18 +470,24 @@ func shareGCPPrinter(context *cli.Context) { sharedWith = context.String("email") } if err != nil { - fmt.Printf("Failed to share GCP printer %s with %s: %s\n", context.String("printer-id"), sharedWith, err) - } else { - fmt.Printf("Shared GCP printer %s with %s\n", context.String("printer-id"), sharedWith) + return fmt.Errorf("Failed to share GCP printer %s with %s: %s\n", context.String("printer-id"), sharedWith, err) } + fmt.Printf("Shared GCP printer %s with %s\n", context.String("printer-id"), sharedWith) + return nil } // unshareGCPPrinter unshares a GCP printer. -func unshareGCPPrinter(context *cli.Context) { - config := getConfig(context) - gcpConn := getGCP(config) +func unshareGCPPrinter(context *cli.Context) error { + config, err := getConfig(context) + if err != nil { + return err + } + gcpConn, err := getGCP(config) + if err != nil { + return err + } - err := gcpConn.Unshare(context.String("printer-id"), context.String("email"), context.Bool("public")) + err = gcpConn.Unshare(context.String("printer-id"), context.String("email"), context.Bool("public")) var sharedWith string if context.Bool("public") { sharedWith = "public" @@ -534,16 +495,22 @@ func unshareGCPPrinter(context *cli.Context) { sharedWith = context.String("email") } if err != nil { - fmt.Printf("Failed to unshare GCP printer %s with %s: %s\n", context.String("printer-id"), sharedWith, err) - } else { - fmt.Printf("Unshared GCP printer %s with %s\n", context.String("printer-id"), sharedWith) + return fmt.Errorf("Failed to unshare GCP printer %s with %s: %s\n", context.String("printer-id"), sharedWith, err) } + fmt.Printf("Unshared GCP printer %s with %s\n", context.String("printer-id"), sharedWith) + return nil } // updateGCPPrinter updates settings for a GCP printer. -func updateGCPPrinter(context *cli.Context) { - config := getConfig(context) - gcpConn := getGCP(config) +func updateGCPPrinter(context *cli.Context) error { + config, err := getConfig(context) + if err != nil { + return err + } + gcpConn, err := getGCP(config) + if err != nil { + return err + } var diff lib.PrinterDiff diff.Printer = lib.Printer{GCPID: context.String("printer-id")} @@ -559,10 +526,11 @@ func updateGCPPrinter(context *cli.Context) { diff.Printer.DailyQuota = context.Int("daily-quota") diff.DailyQuotaChanged = true } - err := gcpConn.Update(&diff) + err = gcpConn.Update(&diff) if err != nil { - fmt.Printf("Failed to update GCP printer %s: %s", context.String("printer-id"), err) + return fmt.Errorf("Failed to update GCP printer %s: %s", context.String("printer-id"), err) } else { fmt.Printf("Updated GCP printer %s", context.String("printer-id")) } + return nil } diff --git a/gcp-connector-util/init.go b/gcp-connector-util/init.go index c1c57a9..70952f6 100644 --- a/gcp-connector-util/init.go +++ b/gcp-connector-util/init.go @@ -9,19 +9,21 @@ https://developers.google.com/open-source/licenses/bsd package main import ( + "bufio" "encoding/json" "fmt" - "log" "net/http" "net/url" "os" "path/filepath" + "runtime" "strings" "time" - "github.com/codegangsta/cli" - "github.com/google/cups-connector/gcp" - "github.com/google/cups-connector/lib" + "github.com/google/cloud-print-connector/gcp" + "github.com/google/cloud-print-connector/lib" + "github.com/google/uuid" + "github.com/urfave/cli" "golang.org/x/oauth2" ) @@ -33,88 +35,150 @@ const ( ) var commonInitFlags = []cli.Flag{ - cli.DurationFlag{ + &cli.StringFlag{ + Name: "gcp-oauth-token-poll-url", + Usage: "OAuth token poll URL", + Value: gcpOAuthTokenPollURL, + }, + &cli.StringFlag{ + Name: "gcp-base-url", + Usage: "GCP base URL", + Value: lib.DefaultConfig.GCPBaseURL, + }, + &cli.StringFlag{ + Name: "gcp-oauth-device-code-url", + Usage: "OAuth device code URL", + Value: gcpOAuthDeviceCodeURL, + }, + &cli.StringFlag{ + Name: "gcp-oauth-token-url", + Usage: "OAuth token URL", + Value: lib.DefaultConfig.GCPOAuthTokenURL, + }, + &cli.StringFlag{ + Name: "gcp-oauth-auth-url", + Usage: "OAuth auth URL", + Value: lib.DefaultConfig.GCPOAuthAuthURL, + }, + &cli.DurationFlag{ Name: "gcp-api-timeout", Usage: "GCP API timeout, for debugging", Value: 30 * time.Second, }, - - cli.StringFlag{ + &cli.StringFlag{ + Name: "gcp-oauth-client-id", + Usage: "Identifies the CUPS Connector to the Google Cloud Print cloud service", + Value: lib.DefaultConfig.GCPOAuthClientID, + }, + &cli.StringFlag{ + Name: "gcp-oauth-client-secret", + Usage: "Goes along with the Client ID. Not actually secret", + Value: lib.DefaultConfig.GCPOAuthClientSecret, + }, + &cli.StringFlag{ Name: "gcp-user-refresh-token", Usage: "GCP user refresh token, useful when managing many connectors", }, - cli.StringFlag{ + &cli.StringFlag{ Name: "share-scope", Usage: "Scope (user or group email address) to automatically share printers with", }, - cli.StringFlag{ + &cli.StringFlag{ Name: "proxy-name", Usage: "Name for this connector instance. Should be unique per Google user account", }, - cli.IntFlag{ + &cli.IntFlag{ Name: "xmpp-port", Usage: "XMPP port number", Value: int(lib.DefaultConfig.XMPPPort), }, - cli.StringFlag{ + &cli.StringFlag{ Name: "xmpp-ping-timeout", Usage: "XMPP ping timeout (give up waiting for ping response after this)", Value: lib.DefaultConfig.XMPPPingTimeout, }, - cli.StringFlag{ + &cli.StringFlag{ Name: "xmpp-ping-interval", Usage: "XMPP ping interval (ping every this often)", Value: lib.DefaultConfig.XMPPPingInterval, }, - cli.IntFlag{ + &cli.IntFlag{ Name: "gcp-max-concurrent-downloads", Usage: "Maximum quantity of PDFs to download concurrently from GCP cloud service", Value: int(lib.DefaultConfig.GCPMaxConcurrentDownloads), }, - cli.IntFlag{ + &cli.IntFlag{ Name: "native-job-queue-size", Usage: "Native job queue size", Value: int(lib.DefaultConfig.NativeJobQueueSize), }, - cli.StringFlag{ + &cli.StringFlag{ Name: "native-printer-poll-interval", Usage: "Interval, in seconds, between native printer state polls", Value: lib.DefaultConfig.NativePrinterPollInterval, }, - cli.BoolFlag{ + &cli.BoolFlag{ Name: "prefix-job-id-to-job-title", Usage: "Whether to add the job ID to the beginning of the job title", }, - cli.StringFlag{ + &cli.StringFlag{ Name: "display-name-prefix", Usage: "Prefix to add to GCP printer's display name", Value: lib.DefaultConfig.DisplayNamePrefix, }, - cli.BoolFlag{ + &cli.BoolFlag{ Name: "local-printing-enable", Usage: "Enable local discovery and printing (aka GCP 2.0 or Privet)", }, - cli.BoolFlag{ + &cli.BoolFlag{ Name: "cloud-printing-enable", Usage: "Enable cloud discovery and printing", }, - cli.StringFlag{ + &cli.StringFlag{ Name: "log-level", - Usage: "Minimum event severity to log: PANIC, ERROR, WARN, INFO, DEBUG, VERBOSE", + Usage: "Minimum event severity to log: FATAL, ERROR, WARNING, INFO, DEBUG", Value: lib.DefaultConfig.LogLevel, }, + &cli.IntFlag{ + Name: "local-port-low", + Usage: "Local HTTP API server port range, low", + Value: int(lib.DefaultConfig.LocalPortLow), + }, + &cli.IntFlag{ + Name: "local-port-high", + Usage: "Local HTTP API server port range, high", + Value: int(lib.DefaultConfig.LocalPortHigh), + }, +} + +func postWithRetry(url string, data url.Values) (*http.Response, error) { + backoff := lib.Backoff{} + for { + response, err := http.PostForm(url, data) + if err == nil { + return response, err + } + fmt.Printf("POST to %s failed with error: %s\n", url, err) + + p, retryAgain := backoff.Pause() + if !retryAgain { + return response, err + } + fmt.Printf("retrying POST to %s in %s\n", url, p) + time.Sleep(p) + } } // getUserClientFromUser follows the token acquisition steps outlined here: // https://developers.google.com/identity/protocols/OAuth2ForDevices -func getUserClientFromUser(context *cli.Context) (*http.Client, string) { +func getUserClientFromUser(context *cli.Context) (*http.Client, string, error) { form := url.Values{ - "client_id": {lib.DefaultConfig.GCPOAuthClientID}, + "client_id": {context.String("gcp-oauth-client-id")}, "scope": {gcp.ScopeCloudPrint}, } - response, err := http.PostForm(gcpOAuthDeviceCodeURL, form) + response, err := postWithRetry(context.String("gcp-oauth-device-code-url"), form) if err != nil { - log.Fatalln(err) + return nil, "", err } var r struct { @@ -132,13 +196,13 @@ func getUserClientFromUser(context *cli.Context) (*http.Client, string) { return pollOAuthConfirmation(context, r.DeviceCode, r.Interval) } -func pollOAuthConfirmation(context *cli.Context, deviceCode string, interval int) (*http.Client, string) { +func pollOAuthConfirmation(context *cli.Context, deviceCode string, interval int) (*http.Client, string, error) { config := oauth2.Config{ - ClientID: lib.DefaultConfig.GCPOAuthClientID, - ClientSecret: lib.DefaultConfig.GCPOAuthClientSecret, + ClientID: context.String("gcp-oauth-client-id"), + ClientSecret: context.String("gcp-oauth-client-secret"), Endpoint: oauth2.Endpoint{ - AuthURL: lib.DefaultConfig.GCPOAuthAuthURL, - TokenURL: lib.DefaultConfig.GCPOAuthTokenURL, + AuthURL: context.String("gcp-oauth-auth-url"), + TokenURL: context.String("gcp-oauth-token-url"), }, RedirectURL: gcp.RedirectURL, Scopes: []string{gcp.ScopeCloudPrint}, @@ -148,14 +212,14 @@ func pollOAuthConfirmation(context *cli.Context, deviceCode string, interval int time.Sleep(time.Duration(interval) * time.Second) form := url.Values{ - "client_id": {lib.DefaultConfig.GCPOAuthClientID}, - "client_secret": {lib.DefaultConfig.GCPOAuthClientSecret}, + "client_id": {context.String("gcp-oauth-client-id")}, + "client_secret": {context.String("gcp-oauth-client-secret")}, "code": {deviceCode}, "grant_type": {gcpOAuthGrantTypeDevice}, } - response, err := http.PostForm(gcpOAuthTokenPollURL, form) + response, err := postWithRetry(context.String("gcp-oauth-token-poll-url"), form) if err != nil { - log.Fatalln(err) + return nil, "", err } var r struct { @@ -171,12 +235,12 @@ func pollOAuthConfirmation(context *cli.Context, deviceCode string, interval int token := &oauth2.Token{RefreshToken: r.RefreshToken} client := config.Client(oauth2.NoContext, token) client.Timeout = context.Duration("gcp-api-timeout") - return client, r.RefreshToken + return client, r.RefreshToken, nil case "authorization_pending": case "slow_down": interval *= 2 default: - log.Fatalln(err) + return nil, "", err } } @@ -186,8 +250,8 @@ func pollOAuthConfirmation(context *cli.Context, deviceCode string, interval int // getUserClientFromToken creates a user client with just a refresh token. func getUserClientFromToken(context *cli.Context) *http.Client { config := &oauth2.Config{ - ClientID: lib.DefaultConfig.GCPOAuthClientID, - ClientSecret: lib.DefaultConfig.GCPOAuthClientSecret, + ClientID: context.String("gcp-oauth-client-id"), + ClientSecret: context.String("gcp-oauth-client-secret"), Endpoint: oauth2.Endpoint{ AuthURL: lib.DefaultConfig.GCPOAuthAuthURL, TokenURL: lib.DefaultConfig.GCPOAuthTokenURL, @@ -204,17 +268,16 @@ func getUserClientFromToken(context *cli.Context) *http.Client { } // initRobotAccount creates a GCP robot account for this connector. -func initRobotAccount(context *cli.Context, userClient *http.Client) (string, string) { +func initRobotAccount(context *cli.Context, userClient *http.Client) (string, string, error) { params := url.Values{} - params.Set("oauth_client_id", lib.DefaultConfig.GCPOAuthClientID) - - url := fmt.Sprintf("%s%s?%s", lib.DefaultConfig.GCPBaseURL, "createrobot", params.Encode()) + params.Set("oauth_client_id", context.String("gcp-oauth-client-id")) + url := fmt.Sprintf("%s%s?%s", context.String("gcp-base-url"), "createrobot", params.Encode()) response, err := userClient.Get(url) if err != nil { - log.Fatalln(err) + return "", "", err } if response.StatusCode != http.StatusOK { - log.Fatalf("Failed to initialize robot account: %s\n", response.Status) + return "", "", fmt.Errorf("Failed to initialize robot account: %s", response.Status) } var robotInit struct { @@ -225,22 +288,22 @@ func initRobotAccount(context *cli.Context, userClient *http.Client) (string, st } if err = json.NewDecoder(response.Body).Decode(&robotInit); err != nil { - log.Fatalln(err) + return "", "", err } if !robotInit.Success { - log.Fatalf("Failed to initialize robot account: %s\n", robotInit.Message) + return "", "", fmt.Errorf("Failed to initialize robot account: %s", robotInit.Message) } - return robotInit.XMPPJID, robotInit.AuthCode + return robotInit.XMPPJID, robotInit.AuthCode, nil } -func verifyRobotAccount(authCode string) string { +func verifyRobotAccount(context *cli.Context, authCode string) (string, error) { config := &oauth2.Config{ - ClientID: lib.DefaultConfig.GCPOAuthClientID, - ClientSecret: lib.DefaultConfig.GCPOAuthClientSecret, + ClientID: context.String("gcp-oauth-client-id"), + ClientSecret: context.String("gcp-oauth-client-secret"), Endpoint: oauth2.Endpoint{ - AuthURL: lib.DefaultConfig.GCPOAuthAuthURL, - TokenURL: lib.DefaultConfig.GCPOAuthTokenURL, + AuthURL: context.String("gcp-oauth-auth-url"), + TokenURL: context.String("gcp-oauth-token-url"), }, RedirectURL: gcp.RedirectURL, Scopes: []string{gcp.ScopeCloudPrint, gcp.ScopeGoogleTalk}, @@ -248,51 +311,46 @@ func verifyRobotAccount(authCode string) string { token, err := config.Exchange(oauth2.NoContext, authCode) if err != nil { - log.Fatalln(err) + return "", err } - return token.RefreshToken + return token.RefreshToken, nil } -func createRobotAccount(context *cli.Context, userClient *http.Client) (string, string) { - xmppJID, authCode := initRobotAccount(context, userClient) - token := verifyRobotAccount(authCode) +func createRobotAccount(context *cli.Context, userClient *http.Client) (string, string, error) { + xmppJID, authCode, err := initRobotAccount(context, userClient) + if err != nil { + return "", "", err + } + token, err := verifyRobotAccount(context, authCode) + if err != nil { + return "", "", err + } - return xmppJID, token + return xmppJID, token, nil } -func writeConfigFile(context *cli.Context, config *lib.Config) string { - if configFilename, err := config.ToFile(context); err != nil { - log.Fatalln(err) +func scanString(prompt string) (string, error) { + fmt.Println(prompt) + reader := bufio.NewReader(os.Stdin) + if answer, err := reader.ReadString('\n'); err != nil { + return "", err } else { - return configFilename - } - panic("unreachable") -} - -func scanNonEmptyString(prompt string) string { - for { - var answer string - fmt.Println(prompt) - if length, err := fmt.Scan(&answer); err != nil { - log.Fatalln(err) - } else if length > 0 { - fmt.Println("") - return answer - } + answer = strings.TrimSpace(answer) // remove newline + fmt.Println("") + return answer, nil } - panic("unreachable") } -func scanYesOrNo(question string) bool { +func scanYesOrNo(question string) (bool, error) { for { var answer string fmt.Println(question) if _, err := fmt.Scan(&answer); err != nil { - log.Fatalln(err) + return false, err } else if parsed, value := stringToBool(answer); parsed { fmt.Println("") - return value + return value, nil } } panic("unreachable") @@ -314,70 +372,101 @@ func stringToBool(val string) (bool, bool) { return false, false } -func initConfigFile(context *cli.Context) { +func initConfigFile(context *cli.Context) error { + var err error + var localEnable bool - if context.IsSet("local-printing-enable") { + if runtime.GOOS == "windows" { + // Remove this if block when Privet support is added to Windows. + localEnable = false + } else if context.IsSet("local-printing-enable") { localEnable = context.Bool("local-printing-enable") } else { - fmt.Println("\"Local printing\" means that clients print directly to the connector via local subnet,") - fmt.Println("and that an Internet connection is neither necessary nor used.") - localEnable = scanYesOrNo("Enable local printing?") + fmt.Println("\"Local printing\" means that clients print directly to the connector via") + fmt.Println("local subnet, and that an Internet connection is neither necessary nor used.") + localEnable, err = scanYesOrNo("Enable local printing?") + if err != nil { + return err + } } var cloudEnable bool - if context.IsSet("cloud-printing-enable") { + if runtime.GOOS == "windows" { + // Remove this if block when Privet support is added to Windows. + cloudEnable = true + } else if localEnable == false { + cloudEnable = true + } else if context.IsSet("cloud-printing-enable") { cloudEnable = context.Bool("cloud-printing-enable") } else { fmt.Println("\"Cloud printing\" means that clients can print from anywhere on the Internet,") fmt.Println("and that printers must be explicitly shared with users.") - cloudEnable = scanYesOrNo("Enable cloud printing?") - } - - if !localEnable && !cloudEnable { - log.Fatalln("Try again. Either local or cloud (or both) must be enabled for the connector to do something.") + cloudEnable, err = scanYesOrNo("Enable cloud printing?") + if err != nil { + return err + } } var config *lib.Config var xmppJID, robotRefreshToken, userRefreshToken, shareScope, proxyName string if cloudEnable { - if context.IsSet("share-scope") { - shareScope = context.String("share-scope") - } else if scanYesOrNo("Retain the user OAuth token to enable automatic sharing?") { - shareScope = scanNonEmptyString("User or group email address to share with:") - } - if context.IsSet("proxy-name") { proxyName = context.String("proxy-name") } else { - proxyName = scanNonEmptyString("Proxy name for this connector:") + v4, err := uuid.NewRandom() + if err != nil { + return err + } + proxyName = v4.String() } var userClient *http.Client + var urt string if context.IsSet("gcp-user-refresh-token") { userClient = getUserClientFromToken(context) - if shareScope != "" { - userRefreshToken = context.String("gcp-user-refresh-token") - } } else { - var urt string - userClient, urt = getUserClientFromUser(context) - if shareScope != "" { - userRefreshToken = urt + userClient, urt, err = getUserClientFromUser(context) + if err != nil { + return err } } - xmppJID, robotRefreshToken = createRobotAccount(context, userClient) + xmppJID, robotRefreshToken, err = createRobotAccount(context, userClient) + if err != nil { + return err + } fmt.Println("Acquired OAuth credentials for robot account") fmt.Println("") + + if context.IsSet("share-scope") { + shareScope = context.String("share-scope") + } else { + shareScope, err = scanString("Enter the email address of a user or group with whom all printers will automatically be shared or leave blank to disable automatic sharing:") + if err != nil { + return err + } + } + + if shareScope != "" { + if context.IsSet("gcp-user-refresh-token") { + userRefreshToken = context.String("gcp-user-refresh-token") + } else { + userRefreshToken = urt + } + } + config = createCloudConfig(context, xmppJID, robotRefreshToken, userRefreshToken, shareScope, proxyName, localEnable) } else { config = createLocalConfig(context) } - configFilename := writeConfigFile(context, config) + configFilename, err := config.Sparse(context).ToFile(context) + if err != nil { + return err + } fmt.Printf("The config file %s is ready to rock.\n", configFilename) if cloudEnable { fmt.Println("Keep it somewhere safe, as it contains an OAuth refresh token.") @@ -387,5 +476,8 @@ func initConfigFile(context *cli.Context) { if _, err := os.Stat(socketDirectory); os.IsNotExist(err) { fmt.Println("") fmt.Printf("When the connector runs, be sure the socket directory %s exists.\n", socketDirectory) + } else if err != nil { + return err } + return nil } diff --git a/gcp-connector-util/main_unix.go b/gcp-connector-util/main_unix.go index c3a00c9..6dd320d 100644 --- a/gcp-connector-util/main_unix.go +++ b/gcp-connector-util/main_unix.go @@ -4,88 +4,89 @@ // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd -// +build linux darwin +// +build linux darwin freebsd package main import ( - "fmt" - "log" "os" "time" - "github.com/codegangsta/cli" - "github.com/google/cups-connector/lib" + "github.com/google/cloud-print-connector/lib" + "github.com/urfave/cli" ) var unixInitFlags = []cli.Flag{ - cli.StringFlag{ + &cli.StringFlag{ Name: "log-file-name", Usage: "Log file name, full path", Value: lib.DefaultConfig.LogFileName, }, - cli.IntFlag{ + &cli.IntFlag{ Name: "log-file-max-megabytes", Usage: "Log file max size, in megabytes", Value: int(lib.DefaultConfig.LogFileMaxMegabytes), }, - cli.IntFlag{ + &cli.IntFlag{ Name: "log-max-files", Usage: "Maximum log file quantity before rollover", Value: int(lib.DefaultConfig.LogMaxFiles), }, - cli.BoolFlag{ + &cli.BoolFlag{ Name: "log-to-journal", Usage: "Log to the systemd journal (if available) instead of to log-file-name", }, - cli.StringFlag{ + &cli.StringFlag{ Name: "monitor-socket-filename", Usage: "Filename of unix socket for connector-check to talk to connector", Value: lib.DefaultConfig.MonitorSocketFilename, }, - cli.IntFlag{ + &cli.IntFlag{ Name: "cups-max-connections", Usage: "Max connections to CUPS server", Value: int(lib.DefaultConfig.CUPSMaxConnections), }, - cli.StringFlag{ + &cli.StringFlag{ Name: "cups-connect-timeout", Usage: "CUPS timeout for opening a new connection", Value: lib.DefaultConfig.CUPSConnectTimeout, }, - cli.BoolFlag{ + &cli.BoolFlag{ Name: "cups-job-full-username", Usage: "Whether to use the full username (joe@example.com) in CUPS jobs", }, - cli.BoolTFlag{ + &cli.BoolFlag{ Name: "cups-ignore-raw-printers", Usage: "Whether to ignore CUPS raw printers", + DefaultText: "1", }, - cli.BoolTFlag{ + &cli.BoolFlag{ Name: "cups-ignore-class-printers", Usage: "Whether to ignore CUPS class printers", + DefaultText: "1", }, - cli.BoolTFlag{ + &cli.BoolFlag{ Name: "copy-printer-info-to-display-name", Usage: "Whether to copy the CUPS printer's printer-info attribute to the GCP printer's defaultDisplayName", + DefaultText: "1", }, } -var unixCommands = []cli.Command{ - cli.Command{ +var unixCommands = []*cli.Command{ + &cli.Command{ Name: "init", - ShortName: "i", + Aliases: []string{"i"}, Usage: "Creates a config file", Action: initConfigFile, Flags: append(commonInitFlags, unixInitFlags...), }, - cli.Command{ + &cli.Command{ Name: "monitor", - ShortName: "m", + Aliases: []string{"m"}, Usage: "Read stats from a running connector", Action: monitorConnector, Flags: []cli.Flag{ - cli.DurationFlag{ + &cli.DurationFlag{ Name: "monitor-timeout", Usage: "wait for a monitor response no more than this long", Value: 10 * time.Second, @@ -94,96 +95,13 @@ var unixCommands = []cli.Command{ }, } -func updateConfig(config *lib.Config, configMap map[string]interface{}) bool { - dirty := commonUpdateConfig(config, configMap) - - if _, exists := configMap["log_file_name"]; !exists { - dirty = true - fmt.Println("Added log_file_name") - config.LogFileName = lib.DefaultConfig.LogFileName - } - if _, exists := configMap["log_file_max_megabytes"]; !exists { - dirty = true - fmt.Println("Added log_file_max_megabytes") - config.LogFileMaxMegabytes = lib.DefaultConfig.LogFileMaxMegabytes - } - if _, exists := configMap["log_max_files"]; !exists { - dirty = true - fmt.Println("Added log_max_files") - config.LogMaxFiles = lib.DefaultConfig.LogMaxFiles - } - if _, exists := configMap["log_to_journal"]; !exists { - dirty = true - fmt.Println("Added log_to_journal") - config.LogToJournal = lib.DefaultConfig.LogToJournal - } - if _, exists := configMap["monitor_socket_filename"]; !exists { - dirty = true - fmt.Println("Added monitor_socket_filename") - config.MonitorSocketFilename = lib.DefaultConfig.MonitorSocketFilename - } - if _, exists := configMap["cups_max_connections"]; !exists { - dirty = true - fmt.Println("Added cups_max_connections") - config.CUPSMaxConnections = lib.DefaultConfig.CUPSMaxConnections - } - if _, exists := configMap["cups_connect_timeout"]; !exists { - dirty = true - fmt.Println("Added cups_connect_timeout") - config.CUPSConnectTimeout = lib.DefaultConfig.CUPSConnectTimeout - } - if _, exists := configMap["cups_printer_attributes"]; !exists { - dirty = true - fmt.Println("Added cups_printer_attributes") - config.CUPSPrinterAttributes = lib.DefaultConfig.CUPSPrinterAttributes - } else { - // Make sure all required attributes are present. - s := make(map[string]struct{}, len(config.CUPSPrinterAttributes)) - for _, a := range config.CUPSPrinterAttributes { - s[a] = struct{}{} - } - for _, a := range lib.DefaultConfig.CUPSPrinterAttributes { - if _, exists := s[a]; !exists { - dirty = true - fmt.Printf("Added %s to cups_printer_attributes\n", a) - config.CUPSPrinterAttributes = append(config.CUPSPrinterAttributes, a) - } - } - } - if _, exists := configMap["cups_job_full_username"]; !exists { - dirty = true - fmt.Println("Added cups_job_full_username") - config.CUPSJobFullUsername = lib.DefaultConfig.CUPSJobFullUsername - } - if _, exists := configMap["cups_ignore_raw_printers"]; !exists { - dirty = true - fmt.Println("Added cups_ignore_raw_printers") - config.CUPSIgnoreRawPrinters = lib.DefaultConfig.CUPSIgnoreRawPrinters - } - if _, exists := configMap["cups_ignore_class_printers"]; !exists { - dirty = true - fmt.Println("Added cups_ignore_class_printers") - config.CUPSIgnoreClassPrinters = lib.DefaultConfig.CUPSIgnoreClassPrinters - } - if _, exists := configMap["copy_printer_info_to_display_name"]; !exists { - dirty = true - fmt.Println("Added copy_printer_info_to_display_name") - config.CUPSCopyPrinterInfoToDisplayName = lib.DefaultConfig.CUPSCopyPrinterInfoToDisplayName - } - - return dirty -} - func main() { - // Suppress date/time prefix. - log.SetFlags(0) - app := cli.NewApp() - app.Name = "gcp-cups-connector-util" - app.Usage = "Google Cloud Print CUPS Connector utility tools" + app.Name = "gcp-connector-util" + app.Usage = lib.ConnectorName + " for CUPS utility tools" app.Version = lib.BuildDate app.Flags = []cli.Flag{ - lib.ConfigFilenameFlag, + &lib.ConfigFilenameFlag, } app.Commands = append(unixCommands, commonCommands...) @@ -193,69 +111,80 @@ func main() { // createCloudConfig creates a config object that supports cloud and (optionally) local mode. func createCloudConfig(context *cli.Context, xmppJID, robotRefreshToken, userRefreshToken, shareScope, proxyName string, localEnable bool) *lib.Config { return &lib.Config{ + LocalPrintingEnable: localEnable, + CloudPrintingEnable: true, + XMPPJID: xmppJID, RobotRefreshToken: robotRefreshToken, UserRefreshToken: userRefreshToken, ShareScope: shareScope, ProxyName: proxyName, + FcmServerBindUrl: context.String("fcm-server-bind-url"), XMPPServer: lib.DefaultConfig.XMPPServer, XMPPPort: uint16(context.Int("xmpp-port")), XMPPPingTimeout: context.String("xmpp-ping-timeout"), XMPPPingInterval: context.String("xmpp-ping-interval"), - GCPBaseURL: lib.DefaultConfig.GCPBaseURL, - GCPOAuthClientID: lib.DefaultConfig.GCPOAuthClientID, - GCPOAuthClientSecret: lib.DefaultConfig.GCPOAuthClientSecret, - GCPOAuthAuthURL: lib.DefaultConfig.GCPOAuthAuthURL, - GCPOAuthTokenURL: lib.DefaultConfig.GCPOAuthTokenURL, + GCPBaseURL: context.String("gcp-base-url"), + GCPOAuthClientID: context.String("gcp-oauth-client-id"), + GCPOAuthClientSecret: context.String("gcp-oauth-client-secret"), + GCPOAuthAuthURL: context.String("gcp-oauth-auth-url"), + GCPOAuthTokenURL: context.String("gcp-oauth-token-url"), GCPMaxConcurrentDownloads: uint(context.Int("gcp-max-concurrent-downloads")), NativeJobQueueSize: uint(context.Int("native-job-queue-size")), NativePrinterPollInterval: context.String("native-printer-poll-interval"), - PrefixJobIDToJobTitle: context.Bool("prefix-job-id-to-job-title"), + PrefixJobIDToJobTitle: lib.PointerToBool(context.Bool("prefix-job-id-to-job-title")), DisplayNamePrefix: context.String("display-name-prefix"), PrinterBlacklist: lib.DefaultConfig.PrinterBlacklist, - LocalPrintingEnable: localEnable, - CloudPrintingEnable: true, + PrinterWhitelist: lib.DefaultConfig.PrinterWhitelist, LogLevel: context.String("log-level"), + LocalPortLow: uint16(context.Int("local-port-low")), + LocalPortHigh: uint16(context.Int("local-port-high")), + LogFileName: context.String("log-file-name"), LogFileMaxMegabytes: uint(context.Int("log-file-max-megabytes")), LogMaxFiles: uint(context.Int("log-max-files")), - LogToJournal: context.Bool("log-to-journal"), + LogToJournal: lib.PointerToBool(context.Bool("log-to-journal")), MonitorSocketFilename: context.String("monitor-socket-filename"), CUPSMaxConnections: uint(context.Int("cups-max-connections")), CUPSConnectTimeout: context.String("cups-connect-timeout"), CUPSPrinterAttributes: lib.DefaultConfig.CUPSPrinterAttributes, - CUPSJobFullUsername: context.Bool("cups-job-full-username"), - CUPSIgnoreRawPrinters: context.Bool("cups-ignore-raw-printers"), - CUPSIgnoreClassPrinters: context.Bool("cups-ignore-class-printers"), - CUPSCopyPrinterInfoToDisplayName: context.Bool("cups-copy-printer-info-to-display-name"), + CUPSJobFullUsername: lib.PointerToBool(context.Bool("cups-job-full-username")), + CUPSIgnoreRawPrinters: lib.PointerToBool(context.Bool("cups-ignore-raw-printers")), + CUPSIgnoreClassPrinters: lib.PointerToBool(context.Bool("cups-ignore-class-printers")), + CUPSCopyPrinterInfoToDisplayName: lib.PointerToBool(context.Bool("copy-printer-info-to-display-name")), } } // createLocalConfig creates a config object that supports local mode. func createLocalConfig(context *cli.Context) *lib.Config { return &lib.Config{ + LocalPrintingEnable: true, + CloudPrintingEnable: false, + NativeJobQueueSize: uint(context.Int("native-job-queue-size")), NativePrinterPollInterval: context.String("native-printer-poll-interval"), - PrefixJobIDToJobTitle: context.Bool("prefix-job-id-to-job-title"), + PrefixJobIDToJobTitle: lib.PointerToBool(context.Bool("prefix-job-id-to-job-title")), DisplayNamePrefix: context.String("display-name-prefix"), PrinterBlacklist: lib.DefaultConfig.PrinterBlacklist, - LocalPrintingEnable: true, - CloudPrintingEnable: false, + PrinterWhitelist: lib.DefaultConfig.PrinterWhitelist, LogLevel: context.String("log-level"), + LocalPortLow: uint16(context.Int("local-port-low")), + LocalPortHigh: uint16(context.Int("local-port-high")), + LogFileName: context.String("log-file-name"), LogFileMaxMegabytes: uint(context.Int("log-file-max-megabytes")), LogMaxFiles: uint(context.Int("log-max-files")), - LogToJournal: context.Bool("log-to-journal"), + LogToJournal: lib.PointerToBool(context.Bool("log-to-journal")), MonitorSocketFilename: context.String("monitor-socket-filename"), CUPSMaxConnections: uint(context.Int("cups-max-connections")), CUPSConnectTimeout: context.String("cups-connect-timeout"), CUPSPrinterAttributes: lib.DefaultConfig.CUPSPrinterAttributes, - CUPSJobFullUsername: context.Bool("cups-job-full-username"), - CUPSIgnoreRawPrinters: context.Bool("cups-ignore-raw-printers"), - CUPSIgnoreClassPrinters: context.Bool("cups-ignore-class-printers"), - CUPSCopyPrinterInfoToDisplayName: context.Bool("cups-copy-printer-info-to-display-name"), + CUPSJobFullUsername: lib.PointerToBool(context.Bool("cups-job-full-username")), + CUPSIgnoreRawPrinters: lib.PointerToBool(context.Bool("cups-ignore-raw-printers")), + CUPSIgnoreClassPrinters: lib.PointerToBool(context.Bool("cups-ignore-class-printers")), + CUPSCopyPrinterInfoToDisplayName: lib.PointerToBool(context.Bool("copy-printer-info-to-display-name")), } } diff --git a/gcp-connector-util/main_windows.go b/gcp-connector-util/main_windows.go index d9046bc..ffc8e29 100644 --- a/gcp-connector-util/main_windows.go +++ b/gcp-connector-util/main_windows.go @@ -10,90 +10,83 @@ package main import ( "fmt" - "log" "os" "path/filepath" - "github.com/codegangsta/cli" - "github.com/google/cups-connector/lib" + "github.com/google/cloud-print-connector/lib" + "github.com/urfave/cli" "golang.org/x/sys/windows/svc" "golang.org/x/sys/windows/svc/eventlog" "golang.org/x/sys/windows/svc/mgr" ) -var windowsCommands = []cli.Command{ - cli.Command{ +var windowsCommands = []*cli.Command{ + &cli.Command{ Name: "init", - ShortName: "i", + Aliases: []string{"i"}, Usage: "Create a config file", Action: initConfigFile, Flags: commonInitFlags, }, - cli.Command{ + &cli.Command{ Name: "install-event-log", Usage: "Install registry entries for the event log", Action: installEventLog, }, - cli.Command{ + &cli.Command{ Name: "remove-event-log", Usage: "Remove registry entries for the event log", Action: removeEventLog, }, - cli.Command{ + &cli.Command{ Name: "create-service", Usage: "Create a service in the local service control manager", Action: createService, }, - cli.Command{ + &cli.Command{ Name: "delete-service", Usage: "Delete an existing service in the local service control manager", Action: deleteService, }, - cli.Command{ + &cli.Command{ Name: "start-service", Usage: "Start the service in the local service control manager", Action: startService, }, - cli.Command{ + &cli.Command{ Name: "stop-service", Usage: "Stop the service in the local service control manager", Action: stopService, }, } -func updateConfig(config *lib.Config, configMap map[string]interface{}) bool { - return commonUpdateConfig(config, configMap) -} - -func installEventLog(c *cli.Context) { +func installEventLog(c *cli.Context) error { err := eventlog.InstallAsEventCreate(lib.ConnectorName, eventlog.Error|eventlog.Warning|eventlog.Info) if err != nil { - fmt.Printf("Failed to install event log registry entries: %s\n", err) - os.Exit(1) + return fmt.Errorf("Failed to install event log registry entries: %s", err) } fmt.Println("Event log registry entries installed successfully") + return nil } -func removeEventLog(c *cli.Context) { +func removeEventLog(c *cli.Context) error { err := eventlog.Remove(lib.ConnectorName) if err != nil { - fmt.Printf("Failed to remove event log registry entries: %s\n", err) - os.Exit(1) + return fmt.Errorf("Failed to remove event log registry entries: %s\n", err) } fmt.Println("Event log registry entries removed successfully") + return nil } -func createService(c *cli.Context) { +func createService(c *cli.Context) error { exePath, err := filepath.Abs("gcp-windows-connector.exe") if err != nil { - fmt.Printf("Failed to find the connector executable: %s\n", err) - os.Exit(1) + return fmt.Errorf("Failed to find the connector executable: %s\n", err) } m, err := mgr.Connect() if err != nil { - fmt.Printf("Failed to connect to service control manager: %s\n", err) - os.Exit(1) + return fmt.Errorf("Failed to connect to service control manager: %s\n", err) } defer m.Disconnect() @@ -105,96 +98,87 @@ func createService(c *cli.Context) { } service, err := m.CreateService(lib.ConnectorName, exePath, config) if err != nil { - fmt.Printf("Failed to create service: %s\n", err) - os.Exit(1) + return fmt.Errorf("Failed to create service: %s\n", err) } defer service.Close() fmt.Println("Service created successfully") + return nil } -func deleteService(c *cli.Context) { +func deleteService(c *cli.Context) error { m, err := mgr.Connect() if err != nil { - fmt.Printf("Failed to connect to service control manager: %s\n", err) - os.Exit(1) + return fmt.Errorf("Failed to connect to service control manager: %s\n", err) } defer m.Disconnect() service, err := m.OpenService(lib.ConnectorName) if err != nil { - fmt.Printf("Failed to open service: %s\n", err) - os.Exit(1) + return fmt.Errorf("Failed to open service: %s\n", err) } defer service.Close() err = service.Delete() if err != nil { - fmt.Printf("Failed to delete service: %s\n", err) - os.Exit(1) + return fmt.Errorf("Failed to delete service: %s\n", err) } fmt.Println("Service deleted successfully") + return nil } -func startService(c *cli.Context) { +func startService(c *cli.Context) error { m, err := mgr.Connect() if err != nil { - fmt.Printf("Failed to connect to service control manager: %s\n", err) - os.Exit(1) + return fmt.Errorf("Failed to connect to service control manager: %s\n", err) } defer m.Disconnect() service, err := m.OpenService(lib.ConnectorName) if err != nil { - fmt.Printf("Failed to open service: %s\n", err) - os.Exit(1) + return fmt.Errorf("Failed to open service: %s\n", err) } defer service.Close() err = service.Start() if err != nil { - fmt.Printf("Failed to start service: %s\n", err) - os.Exit(1) + return fmt.Errorf("Failed to start service: %s\n", err) } fmt.Println("Service started successfully") + return nil } -func stopService(c *cli.Context) { +func stopService(c *cli.Context) error { m, err := mgr.Connect() if err != nil { - fmt.Printf("Failed to connect to service control manager: %s\n", err) - os.Exit(1) + return fmt.Errorf("Failed to connect to service control manager: %s\n", err) } defer m.Disconnect() service, err := m.OpenService(lib.ConnectorName) if err != nil { - fmt.Printf("Failed to open service: %s\n", err) - os.Exit(1) + return fmt.Errorf("Failed to open service: %s\n", err) } defer service.Close() _, err = service.Control(svc.Stop) if err != nil { - fmt.Printf("Failed to stop service: %s\n", err) - os.Exit(1) + return fmt.Errorf("Failed to stop service: %s\n", err) } fmt.Printf("Service stopped successfully") + return nil } func main() { - // Suppress date/time prefix. - log.SetFlags(0) - app := cli.NewApp() - app.Name = "gcp-windows-connector-util" + app.Name = "gcp-connector-util" app.Usage = lib.ConnectorName + " for Windows utility tools" app.Version = lib.BuildDate app.Flags = []cli.Flag{ - lib.ConfigFilenameFlag, + &lib.ConfigFilenameFlag, } app.Commands = append(windowsCommands, commonCommands...) @@ -204,43 +188,56 @@ func main() { // createCloudConfig creates a config object that supports cloud and (optionally) local mode. func createCloudConfig(context *cli.Context, xmppJID, robotRefreshToken, userRefreshToken, shareScope, proxyName string, localEnable bool) *lib.Config { return &lib.Config{ + LocalPrintingEnable: localEnable, + CloudPrintingEnable: true, + XMPPJID: xmppJID, RobotRefreshToken: robotRefreshToken, UserRefreshToken: userRefreshToken, ShareScope: shareScope, ProxyName: proxyName, + FcmServerBindUrl: context.String("fcm-server-bind-url"), XMPPServer: lib.DefaultConfig.XMPPServer, XMPPPort: uint16(context.Int("xmpp-port")), XMPPPingTimeout: context.String("xmpp-ping-timeout"), XMPPPingInterval: context.String("xmpp-ping-interval"), GCPBaseURL: lib.DefaultConfig.GCPBaseURL, - GCPOAuthClientID: lib.DefaultConfig.GCPOAuthClientID, - GCPOAuthClientSecret: lib.DefaultConfig.GCPOAuthClientSecret, + GCPOAuthClientID: context.String("gcp-oauth-client-id"), + GCPOAuthClientSecret: context.String("gcp-oauth-client-secret"), GCPOAuthAuthURL: lib.DefaultConfig.GCPOAuthAuthURL, GCPOAuthTokenURL: lib.DefaultConfig.GCPOAuthTokenURL, GCPMaxConcurrentDownloads: uint(context.Int("gcp-max-concurrent-downloads")), NativeJobQueueSize: uint(context.Int("native-job-queue-size")), NativePrinterPollInterval: context.String("native-printer-poll-interval"), - PrefixJobIDToJobTitle: context.Bool("prefix-job-id-to-job-title"), + CUPSJobFullUsername: lib.PointerToBool(context.Bool("cups-job-full-username")), + PrefixJobIDToJobTitle: lib.PointerToBool(context.Bool("prefix-job-id-to-job-title")), DisplayNamePrefix: context.String("display-name-prefix"), PrinterBlacklist: lib.DefaultConfig.PrinterBlacklist, - LocalPrintingEnable: localEnable, - CloudPrintingEnable: true, + PrinterWhitelist: lib.DefaultConfig.PrinterWhitelist, LogLevel: context.String("log-level"), + + LocalPortLow: uint16(context.Int("local-port-low")), + LocalPortHigh: uint16(context.Int("local-port-high")), } } // createLocalConfig creates a config object that supports local mode. func createLocalConfig(context *cli.Context) *lib.Config { return &lib.Config{ + LocalPrintingEnable: true, + CloudPrintingEnable: false, + NativeJobQueueSize: uint(context.Int("native-job-queue-size")), NativePrinterPollInterval: context.String("native-printer-poll-interval"), - PrefixJobIDToJobTitle: context.Bool("prefix-job-id-to-job-title"), + CUPSJobFullUsername: lib.PointerToBool(context.Bool("cups-job-full-username")), + PrefixJobIDToJobTitle: lib.PointerToBool(context.Bool("prefix-job-id-to-job-title")), DisplayNamePrefix: context.String("display-name-prefix"), PrinterBlacklist: lib.DefaultConfig.PrinterBlacklist, - LocalPrintingEnable: true, - CloudPrintingEnable: false, + PrinterWhitelist: lib.DefaultConfig.PrinterWhitelist, LogLevel: context.String("log-level"), + + LocalPortLow: uint16(context.Int("local-port-low")), + LocalPortHigh: uint16(context.Int("local-port-high")), } } diff --git a/gcp-connector-util/monitor.go b/gcp-connector-util/monitor.go index aa14feb..c54658c 100644 --- a/gcp-connector-util/monitor.go +++ b/gcp-connector-util/monitor.go @@ -4,26 +4,25 @@ // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd -// +build linux darwin +// +build linux darwin freebsd package main import ( "fmt" "io/ioutil" - "log" "net" "os" "time" - "github.com/codegangsta/cli" - "github.com/google/cups-connector/lib" + "github.com/google/cloud-print-connector/lib" + "github.com/urfave/cli" ) -func monitorConnector(context *cli.Context) { +func monitorConnector(context *cli.Context) error { config, filename, err := lib.GetConfig(context) if err != nil { - log.Fatalf("Failed to read config file: %s\n", err) + return fmt.Errorf("Failed to read config file: %s", err) } if filename == "" { fmt.Println("No config file was found, so using defaults") @@ -31,31 +30,33 @@ func monitorConnector(context *cli.Context) { if _, err := os.Stat(config.MonitorSocketFilename); err != nil { if !os.IsNotExist(err) { - log.Fatalln(err) + return err } - log.Fatalf( - "No connector is running, or the monitoring socket %s is mis-configured\n", + return fmt.Errorf( + "No connector is running, or the monitoring socket %s is mis-configured", config.MonitorSocketFilename) } timer := time.AfterFunc(context.Duration("monitor-timeout"), func() { - log.Fatalf("Timeout after %s\n", context.Duration("monitor-timeout").String()) + fmt.Fprintf(os.Stderr, "Monitor check timed out after %s", context.Duration("monitor-timeout").String()) + os.Exit(1) }) conn, err := net.DialTimeout("unix", config.MonitorSocketFilename, time.Second) if err != nil { - log.Fatalf( - "No connector is running, or it is not listening to socket %s\n", + return fmt.Errorf( + "No connector is running, or it is not listening to socket %s", config.MonitorSocketFilename) } defer conn.Close() buf, err := ioutil.ReadAll(conn) if err != nil { - log.Fatalln(err) + return err } timer.Stop() fmt.Printf(string(buf)) + return nil } diff --git a/gcp-cups-connector/gcp-cups-connector.go b/gcp-cups-connector/gcp-cups-connector.go index 8215823..ca66ebc 100644 --- a/gcp-cups-connector/gcp-cups-connector.go +++ b/gcp-cups-connector/gcp-cups-connector.go @@ -4,11 +4,12 @@ // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd -// +build linux darwin +// +build linux darwin freebsd package main import ( + "encoding/json" "fmt" "io" "io/ioutil" @@ -17,45 +18,45 @@ import ( "syscall" "time" - "github.com/codegangsta/cli" "github.com/coreos/go-systemd/journal" - "github.com/google/cups-connector/cups" - "github.com/google/cups-connector/gcp" - "github.com/google/cups-connector/lib" - "github.com/google/cups-connector/log" - "github.com/google/cups-connector/manager" - "github.com/google/cups-connector/monitor" - "github.com/google/cups-connector/privet" - "github.com/google/cups-connector/xmpp" + "github.com/google/cloud-print-connector/cups" + "github.com/google/cloud-print-connector/fcm" + "github.com/google/cloud-print-connector/gcp" + "github.com/google/cloud-print-connector/lib" + "github.com/google/cloud-print-connector/log" + "github.com/google/cloud-print-connector/manager" + "github.com/google/cloud-print-connector/monitor" + "github.com/google/cloud-print-connector/notification" + "github.com/google/cloud-print-connector/privet" + "github.com/google/cloud-print-connector/xmpp" + "github.com/urfave/cli" ) func main() { app := cli.NewApp() app.Name = "gcp-cups-connector" - app.Usage = "Google Cloud Print Connector for CUPS" + app.Usage = lib.ConnectorName + " for CUPS" app.Version = lib.BuildDate app.Flags = []cli.Flag{ - lib.ConfigFilenameFlag, - cli.BoolFlag{ + &lib.ConfigFilenameFlag, + &cli.BoolFlag{ Name: "log-to-console", Usage: "Log to STDERR, in addition to configured logging", }, } - app.Action = func(context *cli.Context) { - os.Exit(connector(context)) - } - app.RunAndExitOnError() + app.Action = connector + app.Run(os.Args) } -func connector(context *cli.Context) int { +func connector(context *cli.Context) error { config, configFilename, err := lib.GetConfig(context) if err != nil { - fmt.Fprintf(os.Stderr, "Failed to read config file: %s", err) - return 1 + return cli.NewExitError(fmt.Sprintf("Failed to read config file: %s", err), 1) } - logToJournal := config.LogToJournal && journal.Enabled() + logToJournal := *config.LogToJournal && journal.Enabled() logToConsole := context.Bool("log-to-console") + useFcm := config.FcmNotificationsEnable if logToJournal { log.SetJournalEnabled(true) @@ -69,8 +70,7 @@ func connector(context *cli.Context) int { var logWriter io.Writer logWriter, err = log.NewLogRoller(config.LogFileName, logFileMaxBytes, config.LogMaxFiles) if err != nil { - fmt.Fprintf(os.Stderr, "Failed to start log roller: %s", err) - return 1 + return cli.NewExitError(fmt.Sprintf("Failed to start log roller: %s", err), 1) } if logToConsole { @@ -81,8 +81,7 @@ func connector(context *cli.Context) int { logLevel, ok := log.LevelFromString(config.LogLevel) if !ok { - fmt.Fprintf(os.Stderr, "Log level %s is not recognized", config.LogLevel) - return 1 + return cli.NewExitError(fmt.Sprintf("Log level %s is not recognized", config.LogLevel), 1) } log.SetLevel(logLevel) @@ -91,108 +90,130 @@ func connector(context *cli.Context) int { } else { log.Infof("Using config file %s", configFilename) } + completeConfig, _ := json.MarshalIndent(config, "", " ") + log.Debugf("Config: %s", string(completeConfig)) log.Info(lib.FullName) fmt.Println(lib.FullName) if !config.CloudPrintingEnable && !config.LocalPrintingEnable { - log.Fatal("Cannot run connector with both local_printing_enable and cloud_printing_enable set to false") - return 1 + errStr := "Cannot run connector with both local_printing_enable and cloud_printing_enable set to false" + log.Fatal(errStr) + return cli.NewExitError(errStr, 1) } if _, err := os.Stat(config.MonitorSocketFilename); !os.IsNotExist(err) { + var errStr string if err != nil { - log.Fatalf("Failed to stat monitor socket: %s", err) + errStr = fmt.Sprintf("Failed to stat monitor socket: %s", err) } else { - log.Fatalf( + errStr = fmt.Sprintf( "A connector is already running, or the monitoring socket %s wasn't cleaned up properly", config.MonitorSocketFilename) } - return 1 + log.Fatal(errStr) + return cli.NewExitError(errStr, 1) } jobs := make(chan *lib.Job, 10) - xmppNotifications := make(chan xmpp.PrinterNotification, 5) + notifications := make(chan notification.PrinterNotification, 5) var g *gcp.GoogleCloudPrint var x *xmpp.XMPP + var f *fcm.FCM if config.CloudPrintingEnable { xmppPingTimeout, err := time.ParseDuration(config.XMPPPingTimeout) if err != nil { - log.Fatalf("Failed to parse xmpp ping timeout: %s", err) - return 1 + errStr := fmt.Sprintf("Failed to parse xmpp ping timeout: %s", err) + log.Fatal(errStr) + return cli.NewExitError(errStr, 1) } xmppPingInterval, err := time.ParseDuration(config.XMPPPingInterval) if err != nil { - log.Fatalf("Failed to parse xmpp ping interval default: %s", err) - return 1 + errStr := fmt.Sprintf("Failed to parse xmpp ping interval default: %s", err) + log.Fatalf(errStr) + return cli.NewExitError(errStr, 1) } g, err = gcp.NewGoogleCloudPrint(config.GCPBaseURL, config.RobotRefreshToken, config.UserRefreshToken, config.ProxyName, config.GCPOAuthClientID, config.GCPOAuthClientSecret, config.GCPOAuthAuthURL, config.GCPOAuthTokenURL, - config.GCPMaxConcurrentDownloads, jobs) + config.GCPMaxConcurrentDownloads, jobs, useFcm) if err != nil { log.Fatal(err) - return 1 + return cli.NewExitError(err.Error(), 1) } - - x, err = xmpp.NewXMPP(config.XMPPJID, config.ProxyName, config.XMPPServer, config.XMPPPort, - xmppPingTimeout, xmppPingInterval, g.GetRobotAccessToken, xmppNotifications) - if err != nil { - log.Fatal(err) - return 1 + if useFcm { + f, err = fcm.NewFCM(config.GCPOAuthClientID, config.ProxyName, config.FcmServerBindUrl, g.FcmSubscribe, notifications) + if err != nil { + log.Fatal(err) + return cli.NewExitError(err.Error(), 1) + } + defer f.Quit() + } else { + x, err = xmpp.NewXMPP(config.XMPPJID, config.ProxyName, config.XMPPServer, config.XMPPPort, + xmppPingTimeout, xmppPingInterval, g.GetRobotAccessToken, notifications) + if err != nil { + log.Fatal(err) + return cli.NewExitError(err.Error(), 1) + } + defer x.Quit() } - defer x.Quit() } cupsConnectTimeout, err := time.ParseDuration(config.CUPSConnectTimeout) if err != nil { - log.Fatalf("Failed to parse CUPS connect timeout: %s", err) - return 1 - } - c, err := cups.NewCUPS(config.CUPSCopyPrinterInfoToDisplayName, config.PrefixJobIDToJobTitle, - config.DisplayNamePrefix, config.CUPSPrinterAttributes, config.CUPSMaxConnections, - cupsConnectTimeout, config.PrinterBlacklist, config.CUPSIgnoreRawPrinters, - config.CUPSIgnoreClassPrinters) + errStr := fmt.Sprintf("Failed to parse CUPS connect timeout: %s", err) + log.Fatalf(errStr) + return cli.NewExitError(errStr, 1) + } + c, err := cups.NewCUPS(*config.CUPSCopyPrinterInfoToDisplayName, *config.PrefixJobIDToJobTitle, + config.DisplayNamePrefix, config.CUPSPrinterAttributes, config.CUPSVendorPPDOptions, config.CUPSMaxConnections, + cupsConnectTimeout, config.PrinterBlacklist, config.PrinterWhitelist, *config.CUPSIgnoreRawPrinters, + *config.CUPSIgnoreClassPrinters, useFcm) if err != nil { log.Fatal(err) - return 1 + return cli.NewExitError(err.Error(), 1) } defer c.Quit() var priv *privet.Privet if config.LocalPrintingEnable { if g == nil { - priv, err = privet.NewPrivet(jobs, config.GCPBaseURL, nil) + priv, err = privet.NewPrivet(jobs, config.LocalPortLow, config.LocalPortHigh, config.GCPBaseURL, nil) } else { - priv, err = privet.NewPrivet(jobs, config.GCPBaseURL, g.ProximityToken) + priv, err = privet.NewPrivet(jobs, config.LocalPortLow, config.LocalPortHigh, config.GCPBaseURL, g.ProximityToken) } if err != nil { log.Fatal(err) - return 1 + return cli.NewExitError(err.Error(), 1) } defer priv.Quit() } nativePrinterPollInterval, err := time.ParseDuration(config.NativePrinterPollInterval) if err != nil { - log.Fatalf("Failed to parse CUPS printer poll interval: %s", err) - return 1 + errStr := fmt.Sprintf("Failed to parse CUPS printer poll interval: %s", err) + log.Fatal(errStr) + return cli.NewExitError(errStr, 1) } pm, err := manager.NewPrinterManager(c, g, priv, nativePrinterPollInterval, - config.NativeJobQueueSize, config.CUPSJobFullUsername, config.ShareScope, - jobs, xmppNotifications) + config.NativeJobQueueSize, *config.CUPSJobFullUsername, config.ShareScope, + jobs, notifications, useFcm) if err != nil { log.Fatal(err) - return 1 + return cli.NewExitError(err.Error(), 1) } defer pm.Quit() + // Init FCM client after printers are registered + if useFcm && config.CloudPrintingEnable { + f.Init() + } m, err := monitor.NewMonitor(c, g, priv, pm, config.MonitorSocketFilename) if err != nil { log.Fatal(err) - return 1 + return cli.NewExitError(err.Error(), 1) } defer m.Quit() @@ -215,7 +236,7 @@ func connector(context *cli.Context) int { fmt.Println("") fmt.Println("Shutting down") - return 0 + return nil } // Blocks until Ctrl-C or SIGTERM. diff --git a/gcp-windows-connector/gcp-windows-connector.go b/gcp-windows-connector/gcp-windows-connector.go index 90cc403..5a95c18 100644 --- a/gcp-windows-connector/gcp-windows-connector.go +++ b/gcp-windows-connector/gcp-windows-connector.go @@ -9,17 +9,20 @@ package main import ( + "encoding/json" "fmt" "os" "time" - "github.com/codegangsta/cli" - "github.com/google/cups-connector/gcp" - "github.com/google/cups-connector/lib" - "github.com/google/cups-connector/log" - "github.com/google/cups-connector/manager" - "github.com/google/cups-connector/winspool" - "github.com/google/cups-connector/xmpp" + "github.com/google/cloud-print-connector/fcm" + "github.com/google/cloud-print-connector/gcp" + "github.com/google/cloud-print-connector/lib" + "github.com/google/cloud-print-connector/log" + "github.com/google/cloud-print-connector/manager" + "github.com/google/cloud-print-connector/notification" + "github.com/google/cloud-print-connector/winspool" + "github.com/google/cloud-print-connector/xmpp" + "github.com/urfave/cli" "golang.org/x/sys/windows/svc" "golang.org/x/sys/windows/svc/debug" ) @@ -27,12 +30,12 @@ import ( func main() { app := cli.NewApp() app.Name = "gcp-windows-connector" - app.Usage = "Google Cloud Print Connector for Windows" + app.Usage = lib.ConnectorName + " for Windows" app.Version = lib.BuildDate app.Flags = []cli.Flag{ - lib.ConfigFilenameFlag, + &lib.ConfigFilenameFlag, } - app.Action = RunService + app.Action = runService app.Run(os.Args) } @@ -52,20 +55,23 @@ type service struct { interactive bool } -func RunService(context *cli.Context) { +func runService(context *cli.Context) error { interactive, err := svc.IsAnInteractiveSession() if err != nil { - fmt.Fprintf(os.Stderr, "Failed to detect interactive session: %s\n", err) - os.Exit(1) + return cli.NewExitError(fmt.Sprintf("Failed to detect interactive session: %s", err), 1) } s := service{context, interactive} if interactive { - debug.Run(lib.ConnectorName, &s) + err = debug.Run(lib.ConnectorName, &s) } else { - svc.Run(lib.ConnectorName, &s) + err = svc.Run(lib.ConnectorName, &s) } + if err != nil { + err = cli.NewExitError(err.Error(), 1) + } + return err } func (service *service) Execute(args []string, r <-chan svc.ChangeRequest, s chan<- svc.Status) (bool, uint32) { @@ -100,6 +106,8 @@ func (service *service) Execute(args []string, r <-chan svc.ChangeRequest, s cha } else { log.Infof("Using config file %s", configFilename) } + completeConfig, _ := json.MarshalIndent(config, "", " ") + log.Debugf("Config: %s", string(completeConfig)) log.Info(lib.FullName) @@ -112,10 +120,11 @@ func (service *service) Execute(args []string, r <-chan svc.ChangeRequest, s cha } jobs := make(chan *lib.Job, 10) - xmppNotifications := make(chan xmpp.PrinterNotification, 5) + notifications := make(chan notification.PrinterNotification, 5) var g *gcp.GoogleCloudPrint var x *xmpp.XMPP + var f *fcm.FCM if config.CloudPrintingEnable { xmppPingTimeout, err := time.ParseDuration(config.XMPPPingTimeout) if err != nil { @@ -131,22 +140,30 @@ func (service *service) Execute(args []string, r <-chan svc.ChangeRequest, s cha g, err = gcp.NewGoogleCloudPrint(config.GCPBaseURL, config.RobotRefreshToken, config.UserRefreshToken, config.ProxyName, config.GCPOAuthClientID, config.GCPOAuthClientSecret, config.GCPOAuthAuthURL, config.GCPOAuthTokenURL, - config.GCPMaxConcurrentDownloads, jobs) + config.GCPMaxConcurrentDownloads, jobs, config.FcmNotificationsEnable) if err != nil { log.Fatal(err) return false, 1 } - - x, err = xmpp.NewXMPP(config.XMPPJID, config.ProxyName, config.XMPPServer, config.XMPPPort, - xmppPingTimeout, xmppPingInterval, g.GetRobotAccessToken, xmppNotifications) - if err != nil { - log.Fatal(err) - return false, 1 + if config.FcmNotificationsEnable { + f, err = fcm.NewFCM(config.GCPOAuthClientID, config.ProxyName, config.FcmServerBindUrl, g.FcmSubscribe, notifications) + if err != nil { + log.Fatal(err) + return false, 1 + } + defer f.Quit() + } else { + x, err = xmpp.NewXMPP(config.XMPPJID, config.ProxyName, config.XMPPServer, config.XMPPPort, + xmppPingTimeout, xmppPingInterval, g.GetRobotAccessToken, notifications) + if err != nil { + log.Fatal(err) + return false, 1 + } + defer x.Quit() } - defer x.Quit() } - ws, err := winspool.NewWinSpool(config.PrefixJobIDToJobTitle, config.DisplayNamePrefix, config.PrinterBlacklist) + ws, err := winspool.NewWinSpool(*config.PrefixJobIDToJobTitle, config.DisplayNamePrefix, config.PrinterBlacklist, config.PrinterWhitelist, config.FcmNotificationsEnable) if err != nil { log.Fatal(err) return false, 1 @@ -158,13 +175,28 @@ func (service *service) Execute(args []string, r <-chan svc.ChangeRequest, s cha return false, 1 } pm, err := manager.NewPrinterManager(ws, g, nil, nativePrinterPollInterval, - config.NativeJobQueueSize, false, config.ShareScope, jobs, xmppNotifications) + config.NativeJobQueueSize, *config.CUPSJobFullUsername, config.ShareScope, jobs, notifications, + config.FcmNotificationsEnable) if err != nil { log.Fatal(err) return false, 1 } defer pm.Quit() + // Init FCM client after printers are registered + if config.FcmNotificationsEnable && config.CloudPrintingEnable { + f.Init() + } + statusHandle := svc.StatusHandle() + if statusHandle != 0 { + err = ws.StartPrinterNotifications(statusHandle) + if err != nil { + log.Error(err) + } else { + log.Info("Successfully registered for device notifications.") + } + } + if config.CloudPrintingEnable { if config.LocalPrintingEnable { log.Infof("Ready to rock as proxy '%s' and in local mode", config.ProxyName) @@ -192,6 +224,15 @@ func (service *service) Execute(args []string, r <-chan svc.ChangeRequest, s cha return false, 0 + case svc.DeviceEvent: + log.Infof("Printers change notification received %d.", request.EventType) + // Delay the action to let the OS finish the process or we might + // not see the new printer. Even if we miss it eventually the timed updates + // will pick it up. + time.AfterFunc(time.Second*5, func() { + pm.SyncPrinters(false) + }) + default: log.Errorf("Received unsupported service command from service control manager: %d", request.Cmd) } diff --git a/gcp/gcp.go b/gcp/gcp.go index 78b9b9f..5d3d198 100644 --- a/gcp/gcp.go +++ b/gcp/gcp.go @@ -26,9 +26,9 @@ import ( "golang.org/x/oauth2" - "github.com/google/cups-connector/cdd" - "github.com/google/cups-connector/lib" - "github.com/google/cups-connector/log" + "github.com/google/cloud-print-connector/cdd" + "github.com/google/cloud-print-connector/lib" + "github.com/google/cloud-print-connector/log" ) const ( @@ -42,6 +42,10 @@ const ( ScopeCloudPrint = "https://www.googleapis.com/auth/cloudprint" ScopeGoogleTalk = "https://www.googleapis.com/auth/googletalk" AccessType = "offline" + + // Printer Notification channel constants. + FCP_CHANNEL = "FCM_CHANNEL" + XMPP_CHANNEL = "XMPP_CHANNEL" ) // GoogleCloudPrint is the interface between Go and the Google Cloud Print API. @@ -50,13 +54,14 @@ type GoogleCloudPrint struct { robotClient *http.Client userClient *http.Client proxyName string + useFcm bool jobs chan<- *lib.Job downloadSemaphore *lib.Semaphore } // NewGoogleCloudPrint establishes a connection with GCP, returns a new GoogleCloudPrint object. -func NewGoogleCloudPrint(baseURL, robotRefreshToken, userRefreshToken, proxyName, oauthClientID, oauthClientSecret, oauthAuthURL, oauthTokenURL string, maxConcurrentDownload uint, jobs chan<- *lib.Job) (*GoogleCloudPrint, error) { +func NewGoogleCloudPrint(baseURL, robotRefreshToken, userRefreshToken, proxyName, oauthClientID, oauthClientSecret, oauthAuthURL, oauthTokenURL string, maxConcurrentDownload uint, jobs chan<- *lib.Job, useFcm bool) (*GoogleCloudPrint, error) { robotClient, err := newClient(oauthClientID, oauthClientSecret, oauthAuthURL, oauthTokenURL, robotRefreshToken, ScopeCloudPrint, ScopeGoogleTalk) if err != nil { return nil, err @@ -75,6 +80,7 @@ func NewGoogleCloudPrint(baseURL, robotRefreshToken, userRefreshToken, proxyName robotClient: robotClient, userClient: userClient, proxyName: proxyName, + useFcm: useFcm, jobs: jobs, downloadSemaphore: lib.NewSemaphore(maxConcurrentDownload), } @@ -147,6 +153,7 @@ func (gcp *GoogleCloudPrint) Fetch(gcpID string) ([]Job, error) { responseBody, errorCode, _, err := postWithRetry(gcp.robotClient, gcp.baseURL+"fetch", form) if err != nil { if errorCode == 413 { + log.Debugf("No jobs returned by fetch (413 error)") // 413 means "Zero print jobs returned", which isn't really an error. return []Job{}, nil } @@ -283,6 +290,12 @@ func (gcp *GoogleCloudPrint) Register(printer *lib.Printer) error { form.Set("capabilities", capabilities) form.Set("capsHash", printer.CapsHash) + if gcp.useFcm { + form.Set("notification_channel", FCP_CHANNEL) + } else { + form.Set("notification_channel", XMPP_CHANNEL) + } + sortedKeys := make([]string, 0, len(printer.Tags)) for key := range printer.Tags { sortedKeys = append(sortedKeys, key) @@ -343,6 +356,9 @@ func (gcp *GoogleCloudPrint) Update(diff *lib.PrinterDiff) error { if diff.ConnectorVersionChanged { form.Set("firmware", diff.Printer.ConnectorVersion) } + if diff.NotificationChannelChanged { + form.Set("notification_channel", diff.Printer.NotificationChannel) + } if diff.StateChanged || diff.DescriptionChanged || diff.GCPVersionChanged { semanticState, err := json.Marshal(cdd.CloudDeviceState{Printer: diff.Printer.State}) @@ -407,22 +423,23 @@ func (gcp *GoogleCloudPrint) Printer(gcpID string) (*lib.Printer, uint, error) { var printersData struct { Printers []struct { - ID string `json:"id"` - Name string `json:"name"` - DefaultDisplayName string `json:"defaultDisplayName"` - UUID string `json:"uuid"` - Manufacturer string `json:"manufacturer"` - Model string `json:"model"` - GCPVersion string `json:"gcpVersion"` - SetupURL string `json:"setupUrl"` - SupportURL string `json:"supportUrl"` - UpdateURL string `json:"updateUrl"` - Firmware string `json:"firmware"` - Capabilities cdd.CloudDeviceDescription `json:"capabilities"` - CapsHash string `json:"capsHash"` - Tags []string `json:"tags"` - QueuedJobsCount uint `json:"queuedJobsCount"` - SemanticState cdd.CloudDeviceState `json:"semanticState"` + ID string `json:"id"` + Name string `json:"name"` + DefaultDisplayName string `json:"defaultDisplayName"` + UUID string `json:"uuid"` + Manufacturer string `json:"manufacturer"` + Model string `json:"model"` + GCPVersion string `json:"gcpVersion"` + SetupURL string `json:"setupUrl"` + SupportURL string `json:"supportUrl"` + UpdateURL string `json:"updateUrl"` + Firmware string `json:"firmware"` + Capabilities cdd.CloudDeviceDescription `json:"capabilities"` + CapsHash string `json:"capsHash"` + Tags []string `json:"tags"` + QueuedJobsCount uint `json:"queuedJobsCount"` + SemanticState cdd.CloudDeviceState `json:"semanticState"` + NotificationChannel string `json:"notificationChannel"` } } if err = json.Unmarshal(responseBody, &printersData); err != nil { @@ -443,21 +460,22 @@ func (gcp *GoogleCloudPrint) Printer(gcpID string) (*lib.Printer, uint, error) { } printer := &lib.Printer{ - GCPID: p.ID, - Name: p.Name, - DefaultDisplayName: p.DefaultDisplayName, - UUID: p.UUID, - Manufacturer: p.Manufacturer, - Model: p.Model, - GCPVersion: p.GCPVersion, - SetupURL: p.SetupURL, - SupportURL: p.SupportURL, - UpdateURL: p.UpdateURL, - ConnectorVersion: p.Firmware, - State: p.SemanticState.Printer, - Description: p.Capabilities.Printer, - CapsHash: p.CapsHash, - Tags: tags, + GCPID: p.ID, + Name: p.Name, + DefaultDisplayName: p.DefaultDisplayName, + UUID: p.UUID, + Manufacturer: p.Manufacturer, + Model: p.Model, + GCPVersion: p.GCPVersion, + SetupURL: p.SetupURL, + SupportURL: p.SupportURL, + UpdateURL: p.UpdateURL, + ConnectorVersion: p.Firmware, + State: p.SemanticState.Printer, + Description: p.Capabilities.Printer, + CapsHash: p.CapsHash, + Tags: tags, + NotificationChannel: p.NotificationChannel, } return printer, p.QueuedJobsCount, err @@ -545,6 +563,24 @@ func (gcp *GoogleCloudPrint) Download(dst io.Writer, url string) error { return nil } +// FCM Subscribe. +func (gcp *GoogleCloudPrint) FcmSubscribe(subscribeUrl string) (interface{}, error) { + response, err := getWithRetry(gcp.robotClient, fmt.Sprintf("%s%s", gcp.baseURL, subscribeUrl)) + if err != nil { + return nil, fmt.Errorf("failed to get Fcm Token: %s", err) + } + defer response.Body.Close() + + if response.StatusCode == 200 { + data, _ := ioutil.ReadAll(response.Body) + var f interface{} + json.Unmarshal(data, &f) + return f, nil + } else { + return nil, fmt.Errorf("failed to get Fcm Token: %d", response.StatusCode) + } +} + // Ticket gets a ticket, aka print job options. func (gcp *GoogleCloudPrint) Ticket(gcpJobID string) (*cdd.CloudJobTicket, error) { form := url.Values{} @@ -703,7 +739,7 @@ func (gcp *GoogleCloudPrint) assembleJob(job *Job) (*cdd.CloudJobTicket, string, } } - file, err := ioutil.TempFile("", "cups-connector-gcp-") + file, err := ioutil.TempFile("", "cloud-print-connector-") if err != nil { return nil, "", fmt.Sprintf("Failed to create a temporary file: %s", err), @@ -717,8 +753,13 @@ func (gcp *GoogleCloudPrint) assembleJob(job *Job) (*cdd.CloudJobTicket, string, gcp.downloadSemaphore.Acquire() t := time.Now() + downloadUrl := job.FileURL + if !strings.HasPrefix(downloadUrl, "http") { + // test env url need to prefix with http + downloadUrl = "http://" + job.FileURL + } // Do not check err until semaphore is released and timer is stopped. - err = gcp.Download(file, job.FileURL) + err = gcp.Download(file, downloadUrl) dt := time.Since(t) gcp.downloadSemaphore.Release() if err != nil { diff --git a/gcp/http.go b/gcp/http.go index 99aeb7d..8f41c11 100644 --- a/gcp/http.go +++ b/gcp/http.go @@ -15,8 +15,10 @@ import ( "net/http" "net/url" "strings" + "time" - "github.com/google/cups-connector/lib" + "github.com/google/cloud-print-connector/lib" + "github.com/google/cloud-print-connector/log" "golang.org/x/oauth2" ) @@ -58,15 +60,27 @@ func newClient(oauthClientID, oauthClientSecret, oauthAuthURL, oauthTokenURL, re return client, nil } -// getWithRetry calls get() and retries once on HTTP failure -// (response code != 200). +// getWithRetry calls get() and retries on HTTP temp failure +// (response code 500-599). func getWithRetry(hc *http.Client, url string) (*http.Response, error) { - response, err := get(hc, url) - if response != nil && response.StatusCode == http.StatusOK { - return response, err + backoff := lib.Backoff{} + for { + response, err := get(hc, url) + if response != nil && response.StatusCode == http.StatusOK { + return response, err + } else if response != nil && response.StatusCode >= 500 && response.StatusCode <= 599 { + p, retryAgain := backoff.Pause() + if !retryAgain { + log.Debugf("HTTP error %s, retry timeout hit", err) + return response, err + } + log.Debugf("HTTP error %s, retrying after %s", err, p) + time.Sleep(p) + } else { + log.Debugf("Permanent HTTP error %s, will not retry", err) + return response, err + } } - - return get(hc, url) } // get GETs a URL. Returns the response object (not body), in case the body @@ -84,24 +98,36 @@ func get(hc *http.Client, url string) (*http.Response, error) { response, err := hc.Do(request) lock.Release() if err != nil { - return nil, fmt.Errorf("GET failure: %s", err) + return response, fmt.Errorf("GET failure: %s", err) } if response.StatusCode != http.StatusOK { - return nil, fmt.Errorf("GET HTTP-level failure: %s %s", url, response.Status) + return response, fmt.Errorf("GET HTTP-level failure: %s %s", url, response.Status) } return response, nil } -// postWithRetry calls post() and retries once on HTTP failure -// (response code != 200). +// postWithRetry calls post() and retries on HTTP temp failure +// (response code 500-599). func postWithRetry(hc *http.Client, url string, form url.Values) ([]byte, uint, int, error) { - responseBody, gcpErrorCode, httpStatusCode, err := post(hc, url, form) - if responseBody != nil && httpStatusCode == http.StatusOK { - return responseBody, gcpErrorCode, httpStatusCode, err + backoff := lib.Backoff{} + for { + responseBody, gcpErrorCode, httpStatusCode, err := post(hc, url, form) + if responseBody != nil && httpStatusCode == http.StatusOK { + return responseBody, gcpErrorCode, httpStatusCode, err + } else if responseBody != nil && httpStatusCode >= 500 && httpStatusCode <= 599 { + p, retryAgain := backoff.Pause() + if !retryAgain { + log.Debugf("HTTP error %s, retry timeout hit", err) + return responseBody, gcpErrorCode, httpStatusCode, err + } + log.Debugf("HTTP error %s, retrying after %s", err, p) + time.Sleep(p) + } else { + log.Debugf("Permanent HTTP error %s, will not retry", err) + return responseBody, gcpErrorCode, httpStatusCode, err + } } - - return post(hc, url, form) } // post POSTs to a URL. Returns the body of the response. diff --git a/gcp/job.go b/gcp/job.go index cc94f39..0b03104 100644 --- a/gcp/job.go +++ b/gcp/job.go @@ -8,7 +8,7 @@ https://developers.google.com/open-source/licenses/bsd package gcp -import "github.com/google/cups-connector/cdd" +import "github.com/google/cloud-print-connector/cdd" type Job struct { GCPPrinterID string diff --git a/lib/backoff.go b/lib/backoff.go new file mode 100644 index 0000000..e34bd54 --- /dev/null +++ b/lib/backoff.go @@ -0,0 +1,55 @@ +/* +Copyright 2016 Google Inc. All rights reserved. + +Use of this source code is governed by a BSD-style +license that can be found in the LICENSE file or at +https://developers.google.com/open-source/licenses/bsd +*/ + +package lib + +import ( + "math/rand" + "time" +) + +const ( + initialRetryInterval = 500 * time.Millisecond + maxInterval = 1 * time.Minute + maxElapsedTime = 15 * time.Minute + multiplier = 1.5 + randomizationFactor = 0.5 +) + +// Backoff provides a mechanism for determining a good amount of time before +// retrying an operation. +type Backoff struct { + interval time.Duration + elapsedTime time.Duration +} + +// Pause returns the amount of time to wait before retrying an operation and true if +// it is ok to try again or false if the operation should be abandoned. +func (b *Backoff) Pause() (time.Duration, bool) { + if b.interval == 0 { + // first time + b.interval = initialRetryInterval + b.elapsedTime = 0 + } + + // interval from [1 - randomizationFactor, 1 + randomizationFactor) + randomizedInterval := time.Duration((rand.Float64()*(2*randomizationFactor) + (1 - randomizationFactor)) * float64(b.interval)) + b.elapsedTime += randomizedInterval + + if b.elapsedTime > maxElapsedTime { + return 0, false + } + + // Increase interval up to the interval cap + b.interval = time.Duration(float64(b.interval) * multiplier) + if b.interval > maxInterval { + b.interval = maxInterval + } + + return randomizedInterval, true +} diff --git a/lib/backoff_test.go b/lib/backoff_test.go new file mode 100644 index 0000000..5f0c091 --- /dev/null +++ b/lib/backoff_test.go @@ -0,0 +1,47 @@ +/* +Copyright 2016 Google Inc. All rights reserved. + +Use of this source code is governed by a BSD-style +license that can be found in the LICENSE file or at +https://developers.google.com/open-source/licenses/bsd +*/ + +package lib + +import ( + "testing" + "time" +) + +func TestBackoffMultiple(t *testing.T) { + b := &Backoff{} + // with the current parameters, we will be able to wait at least 19 times before hitting the max + for i := 0; i < 19; i++ { + p, ok := b.Pause() + t.Logf("iteration %d pausing for %s", i, p) + if !ok { + t.Fatalf("hit the pause timeout after %d pauses", i) + } + } +} + +func TestBackoffTimeout(t *testing.T) { + var elapsed time.Duration + b := &Backoff{} + // with the current parameters, we will hit the timeout at or before 40 pauses + for i := 0; i < 40; i++ { + p, ok := b.Pause() + elapsed += p + t.Logf("iteration %d pausing for %s (total %s)", i, p, elapsed) + if !ok { + break + } + } + if _, ok := b.Pause(); ok { + t.Fatalf("did not hit the pause timeout") + } + + if elapsed > maxElapsedTime { + t.Fatalf("waited too long: %s > %s", elapsed, maxElapsedTime) + } +} diff --git a/lib/config.go b/lib/config.go index 70ed1a4..69a5620 100644 --- a/lib/config.go +++ b/lib/config.go @@ -10,18 +10,18 @@ package lib import ( "encoding/json" - "fmt" "io/ioutil" + "reflect" "runtime" - "github.com/codegangsta/cli" + "github.com/urfave/cli" ) const ( - ConnectorName = "Cloud Print Connector" + ConnectorName = "Google Cloud Print Connector" // A website with user-friendly information. - ConnectorHomeURL = "https://github.com/google/cups-connector" + ConnectorHomeURL = "https://github.com/google/cloud-print-connector" GCPAPIVersion = "2.0" ) @@ -29,12 +29,12 @@ const ( var ( ConfigFilenameFlag = cli.StringFlag{ Name: "config-filename", - Usage: fmt.Sprintf("Connector config filename (default \"%s\")", defaultConfigFilename), + Usage: "Connector config filename", Value: defaultConfigFilename, } // To be populated by something like: - // go install -ldflags "-X github.com/google/cups-connector/lib.BuildDate=`date +%Y.%m.%d`" + // go install -ldflags "-X github.com/google/cloud-print-connector/lib.BuildDate=`date +%Y.%m.%d`" BuildDate = "DEV" ShortName = platformName + " Connector " + BuildDate + "-" + runtime.GOOS @@ -42,6 +42,11 @@ var ( FullName = ConnectorName + " for " + platformName + " version " + BuildDate + "-" + runtime.GOOS ) +// PointerToBool converts a boolean value (constant) to a pointer-to-bool. +func PointerToBool(b bool) *bool { + return &b +} + // GetConfig reads a Config object from the config file indicated by the config // filename flag. If no such file exists, then DefaultConfig is returned. func GetConfig(context *cli.Context) (*Config, string, error) { @@ -50,16 +55,25 @@ func GetConfig(context *cli.Context) (*Config, string, error) { return &DefaultConfig, "", nil } - b, err := ioutil.ReadFile(cf) + configRaw, err := ioutil.ReadFile(cf) if err != nil { return nil, "", err } - var config Config - if err = json.Unmarshal(b, &config); err != nil { + config := new(Config) + if err = json.Unmarshal(configRaw, config); err != nil { + return nil, "", err + } + + // Same config as a map so that we can detect missing keys. + var configMap map[string]interface{} + if err = json.Unmarshal(configRaw, &configMap); err != nil { return nil, "", err } - return &config, cf, nil + + b := config.Backfill(configMap) + + return b, cf, nil } // ToFile writes this Config object to the config file indicated by ConfigFile. @@ -75,3 +89,154 @@ func (c *Config) ToFile(context *cli.Context) (string, error) { } return cf, nil } + +func (c *Config) commonSparse(context *cli.Context) *Config { + s := *c + + if s.XMPPServer == DefaultConfig.XMPPServer { + s.XMPPServer = "" + } + if !context.IsSet("xmpp-port") && + s.XMPPPort == DefaultConfig.XMPPPort { + s.XMPPPort = 0 + } + if !context.IsSet("xmpp-ping-timeout") && + s.XMPPPingTimeout == DefaultConfig.XMPPPingTimeout { + s.XMPPPingTimeout = "" + } + if !context.IsSet("xmpp-ping-interval") && + s.XMPPPingInterval == DefaultConfig.XMPPPingInterval { + s.XMPPPingInterval = "" + } + if s.GCPBaseURL == DefaultConfig.GCPBaseURL { + s.GCPBaseURL = "" + } + if s.FcmServerBindUrl == DefaultConfig.FcmServerBindUrl { + s.FcmServerBindUrl = "" + } + if s.GCPOAuthClientID == DefaultConfig.GCPOAuthClientID { + s.GCPOAuthClientID = "" + } + if s.GCPOAuthClientSecret == DefaultConfig.GCPOAuthClientSecret { + s.GCPOAuthClientSecret = "" + } + if s.GCPOAuthAuthURL == DefaultConfig.GCPOAuthAuthURL { + s.GCPOAuthAuthURL = "" + } + if s.GCPOAuthTokenURL == DefaultConfig.GCPOAuthTokenURL { + s.GCPOAuthTokenURL = "" + } + if !context.IsSet("gcp-max-concurrent-downloads") && + s.GCPMaxConcurrentDownloads == DefaultConfig.GCPMaxConcurrentDownloads { + s.GCPMaxConcurrentDownloads = 0 + } + if !context.IsSet("native-job-queue-size") && + s.NativeJobQueueSize == DefaultConfig.NativeJobQueueSize { + s.NativeJobQueueSize = 0 + } + if !context.IsSet("native-printer-poll-interval") && + s.NativePrinterPollInterval == DefaultConfig.NativePrinterPollInterval { + s.NativePrinterPollInterval = "" + } + if !context.IsSet("cups-job-full-username") && + reflect.DeepEqual(s.CUPSJobFullUsername, DefaultConfig.CUPSJobFullUsername) { + s.CUPSJobFullUsername = nil + } + if !context.IsSet("prefix-job-id-to-job-title") && + reflect.DeepEqual(s.PrefixJobIDToJobTitle, DefaultConfig.PrefixJobIDToJobTitle) { + s.PrefixJobIDToJobTitle = nil + } + if !context.IsSet("display-name-prefix") && + s.DisplayNamePrefix == DefaultConfig.DisplayNamePrefix { + s.DisplayNamePrefix = "" + } + if !context.IsSet("local-port-low") && + s.LocalPortLow == DefaultConfig.LocalPortLow { + s.LocalPortLow = 0 + } + if !context.IsSet("local-port-high") && + s.LocalPortHigh == DefaultConfig.LocalPortHigh { + s.LocalPortHigh = 0 + } + + return &s +} + +func (c *Config) commonBackfill(configMap map[string]interface{}) *Config { + b := *c + + if _, exists := configMap["xmpp_server"]; !exists { + b.XMPPServer = DefaultConfig.XMPPServer + } + if _, exists := configMap["xmpp_port"]; !exists { + b.XMPPPort = DefaultConfig.XMPPPort + } + if _, exists := configMap["gcp_xmpp_ping_timeout"]; !exists { + b.XMPPPingTimeout = DefaultConfig.XMPPPingTimeout + } + if _, exists := configMap["gcp_xmpp_ping_interval_default"]; !exists { + b.XMPPPingInterval = DefaultConfig.XMPPPingInterval + } + if _, exists := configMap["gcp_base_url"]; !exists { + b.GCPBaseURL = DefaultConfig.GCPBaseURL + } + if _, exists := configMap["fcm_server_bind_url"]; !exists { + b.FcmServerBindUrl = DefaultConfig.FcmServerBindUrl + } + if _, exists := configMap["gcp_oauth_client_id"]; !exists { + b.GCPOAuthClientID = DefaultConfig.GCPOAuthClientID + } + if _, exists := configMap["gcp_oauth_client_secret"]; !exists { + b.GCPOAuthClientSecret = DefaultConfig.GCPOAuthClientSecret + } + if _, exists := configMap["gcp_oauth_auth_url"]; !exists { + b.GCPOAuthAuthURL = DefaultConfig.GCPOAuthAuthURL + } + if _, exists := configMap["gcp_oauth_token_url"]; !exists { + b.GCPOAuthTokenURL = DefaultConfig.GCPOAuthTokenURL + } + if _, exists := configMap["gcp_max_concurrent_downloads"]; !exists { + b.GCPMaxConcurrentDownloads = DefaultConfig.GCPMaxConcurrentDownloads + } + if _, exists := configMap["cups_job_queue_size"]; !exists { + b.NativeJobQueueSize = DefaultConfig.NativeJobQueueSize + } + if _, exists := configMap["cups_printer_poll_interval"]; !exists { + b.NativePrinterPollInterval = DefaultConfig.NativePrinterPollInterval + } + if _, exists := configMap["cups_job_full_username"]; !exists { + b.CUPSJobFullUsername = DefaultConfig.CUPSJobFullUsername + } + if _, exists := configMap["prefix_job_id_to_job_title"]; !exists { + b.PrefixJobIDToJobTitle = DefaultConfig.PrefixJobIDToJobTitle + } + if _, exists := configMap["display_name_prefix"]; !exists { + b.DisplayNamePrefix = DefaultConfig.DisplayNamePrefix + } + if _, exists := configMap["printer_blacklist"]; !exists { + b.PrinterBlacklist = DefaultConfig.PrinterBlacklist + } + if _, exists := configMap["printer_whitelist"]; !exists { + b.PrinterWhitelist = DefaultConfig.PrinterWhitelist + } + if _, exists := configMap["local_printing_enable"]; !exists { + b.LocalPrintingEnable = DefaultConfig.LocalPrintingEnable + } + if _, exists := configMap["cloud_printing_enable"]; !exists { + b.CloudPrintingEnable = DefaultConfig.CloudPrintingEnable + } + if _, exists := configMap["fcm_notifications_enable"]; !exists { + b.FcmNotificationsEnable = DefaultConfig.FcmNotificationsEnable + } + if _, exists := configMap["log_level"]; !exists { + b.LogLevel = DefaultConfig.LogLevel + } + if _, exists := configMap["local_port_low"]; !exists { + b.LocalPortLow = DefaultConfig.LocalPortLow + } + if _, exists := configMap["local_port_high"]; !exists { + b.LocalPortHigh = DefaultConfig.LocalPortHigh + } + + return &b +} diff --git a/lib/config_unix.go b/lib/config_unix.go index e560c03..b373371 100644 --- a/lib/config_unix.go +++ b/lib/config_unix.go @@ -4,15 +4,16 @@ // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd -// +build linux darwin +// +build linux darwin freebsd package lib import ( "os" "path/filepath" + "reflect" - "github.com/codegangsta/cli" + "github.com/urfave/cli" "launchpad.net/go-xdg/v0" ) @@ -23,6 +24,15 @@ const ( ) type Config struct { + // Enable local discovery and printing. + LocalPrintingEnable bool `json:"local_printing_enable"` + + // Enable cloud discovery and printing. + CloudPrintingEnable bool `json:"cloud_printing_enable"` + + // Enable fcm notifications instead of xmpp notifications. + FcmNotificationsEnable bool `json:"fcm_notifications_enable"` + // Associated with root account. XMPP credential. XMPPJID string `json:"xmpp_jid,omitempty"` @@ -38,6 +48,9 @@ type Config struct { // User-chosen name of this proxy. Should be unique per Google user account. ProxyName string `json:"proxy_name,omitempty"` + // FCM url client should listen on. + FcmServerBindUrl string `json:"fcm_server_bind_url,omitempty"` + // XMPP server FQDN. XMPPServer string `json:"xmpp_server,omitempty"` @@ -71,46 +84,53 @@ type Config struct { // Maximum quantity of jobs (data) to download concurrently. GCPMaxConcurrentDownloads uint `json:"gcp_max_concurrent_downloads,omitempty"` - // CUPS job queue size. + // CUPS job queue size, must be greater than zero. // TODO: rename without cups_ prefix - NativeJobQueueSize uint `json:"cups_job_queue_size"` + NativeJobQueueSize uint `json:"cups_job_queue_size,omitempty"` // Interval (eg 10s, 1m) between CUPS printer state polls. // TODO: rename without cups_ prefix - NativePrinterPollInterval string `json:"cups_printer_poll_interval"` + NativePrinterPollInterval string `json:"cups_printer_poll_interval,omitempty"` + + // Use the full username (joe@example.com) in job. + // TODO: rename without cups_ prefix + CUPSJobFullUsername *bool `json:"cups_job_full_username,omitempty"` // Add the job ID to the beginning of the job title. Useful for debugging. - PrefixJobIDToJobTitle bool `json:"prefix_job_id_to_job_title"` + PrefixJobIDToJobTitle *bool `json:"prefix_job_id_to_job_title,omitempty"` // Prefix for all GCP printers hosted by this connector. - DisplayNamePrefix string `json:"display_name_prefix"` + DisplayNamePrefix string `json:"display_name_prefix,omitempty"` // Ignore printers with native names. - PrinterBlacklist []string `json:"printer_blacklist"` + PrinterBlacklist []string `json:"printer_blacklist,omitempty"` - // Enable local discovery and printing. - LocalPrintingEnable bool `json:"local_printing_enable"` - - // Enable cloud discovery and printing. - CloudPrintingEnable bool `json:"cloud_printing_enable"` + // Allow printers with native names. + PrinterWhitelist []string `json:"printer_whitelist,omitempty"` // Least severity to log. LogLevel string `json:"log_level"` + // Local only: HTTP API port range, low. + LocalPortLow uint16 `json:"local_port_low,omitempty"` + + // Local only: HTTP API port range, high. + LocalPortHigh uint16 `json:"local_port_high,omitempty"` + // CUPS only: Where to place log file. LogFileName string `json:"log_file_name"` // CUPS only: Maximum log file size. - LogFileMaxMegabytes uint `json:"log_file_max_megabytes"` + LogFileMaxMegabytes uint `json:"log_file_max_megabytes,omitempty"` // CUPS only: Maximum log file quantity. - LogMaxFiles uint `json:"log_max_files"` + LogMaxFiles uint `json:"log_max_files,omitempty"` // CUPS only: Log to the systemd journal instead of to files? - LogToJournal bool `json:"log_to_journal"` + LogToJournal *bool `json:"log_to_journal,omitempty"` // CUPS only: Filename of unix socket for connector-check to talk to connector. - MonitorSocketFilename string `json:"monitor_socket_filename"` + MonitorSocketFilename string `json:"monitor_socket_filename,omitempty"` // CUPS only: Maximum quantity of open CUPS connections. CUPSMaxConnections uint `json:"cups_max_connections,omitempty"` @@ -121,28 +141,33 @@ type Config struct { // CUPS only: printer attributes to copy to GCP. CUPSPrinterAttributes []string `json:"cups_printer_attributes,omitempty"` - // CUPS only: use the full username (joe@example.com) in CUPS job. - CUPSJobFullUsername bool `json:"cups_job_full_username,omitempty"` + // CUPS only: non-standard PPD options to add as GCP vendor capabilities. + CUPSVendorPPDOptions []string `json:"cups_vendor_ppd_options,omitempty"` // CUPS only: ignore printers with make/model 'Local Raw Printer'. - CUPSIgnoreRawPrinters bool `json:"cups_ignore_raw_printers"` + CUPSIgnoreRawPrinters *bool `json:"cups_ignore_raw_printers,omitempty"` // CUPS only: ignore printers with make/model 'Local Printer Class'. - CUPSIgnoreClassPrinters bool `json:"cups_ignore_class_printers"` + CUPSIgnoreClassPrinters *bool `json:"cups_ignore_class_printers,omitempty"` // CUPS only: copy the CUPS printer's printer-info attribute to the GCP printer's defaultDisplayName. // TODO: rename with cups_ prefix - CUPSCopyPrinterInfoToDisplayName bool `json:"copy_printer_info_to_display_name,omitempty"` + CUPSCopyPrinterInfoToDisplayName *bool `json:"copy_printer_info_to_display_name,omitempty"` } // DefaultConfig represents reasonable default values for Config fields. // Omitted Config fields are omitted on purpose; they are unique per // connector instance. var DefaultConfig = Config{ + LocalPrintingEnable: true, + CloudPrintingEnable: false, + FcmNotificationsEnable: false, + XMPPServer: "talk.google.com", XMPPPort: 443, XMPPPingTimeout: "5s", XMPPPingInterval: "2m", + FcmServerBindUrl: "https://fcm-stream.googleapis.com/fcm/connect/bind", GCPBaseURL: "https://www.google.com/cloudprint/", GCPOAuthClientID: "539833558011-35iq8btpgas80nrs3o7mv99hm95d4dv6.apps.googleusercontent.com", GCPOAuthClientSecret: "V9BfPOvdiYuw12hDx5Y5nR0a", @@ -152,19 +177,21 @@ var DefaultConfig = Config{ NativeJobQueueSize: 3, NativePrinterPollInterval: "1m", - PrefixJobIDToJobTitle: false, + PrefixJobIDToJobTitle: PointerToBool(false), DisplayNamePrefix: "", PrinterBlacklist: []string{}, - LocalPrintingEnable: true, - CloudPrintingEnable: false, + PrinterWhitelist: []string{}, LogLevel: "INFO", - LogFileName: "/tmp/cups-connector", + LocalPortLow: 26000, + LocalPortHigh: 26999, + + LogFileName: "/tmp/cloud-print-connector", LogFileMaxMegabytes: 1, LogMaxFiles: 3, - LogToJournal: false, + LogToJournal: PointerToBool(false), - MonitorSocketFilename: "/tmp/cups-connector-monitor.sock", + MonitorSocketFilename: "/tmp/cloud-print-connector-monitor.sock", CUPSMaxConnections: 50, CUPSConnectTimeout: "5s", @@ -192,10 +219,10 @@ var DefaultConfig = Config{ "orientation-requested-supported", "pdf-versions-supported", }, - CUPSJobFullUsername: false, - CUPSIgnoreRawPrinters: true, - CUPSIgnoreClassPrinters: true, - CUPSCopyPrinterInfoToDisplayName: true, + CUPSJobFullUsername: PointerToBool(false), + CUPSIgnoreRawPrinters: PointerToBool(true), + CUPSIgnoreClassPrinters: PointerToBool(true), + CUPSCopyPrinterInfoToDisplayName: PointerToBool(true), } // getConfigFilename gets the absolute filename of the config file specified by @@ -205,7 +232,7 @@ var DefaultConfig = Config{ // If the ConfigFilename exists in a valid XDG path, then it is returned. // If neither of those exist, the (relative or absolute) ConfigFilename is returned. func getConfigFilename(context *cli.Context) (string, bool) { - cf := context.GlobalString("config-filename") + cf := context.String("config-filename") if filepath.IsAbs(cf) { // Absolute path specified; user knows what they want. @@ -232,3 +259,109 @@ func getConfigFilename(context *cli.Context) (string, bool) { // it wasn't found anywhere else. return absCF, false } + +// Backfill returns a copy of this config with all missing keys set to default values. +func (c *Config) Backfill(configMap map[string]interface{}) *Config { + b := *c.commonBackfill(configMap) + + if _, exists := configMap["log_file_name"]; !exists { + b.LogFileName = DefaultConfig.LogFileName + } + if _, exists := configMap["log_file_max_megabytes"]; !exists { + b.LogFileMaxMegabytes = DefaultConfig.LogFileMaxMegabytes + } + if _, exists := configMap["log_max_files"]; !exists { + b.LogMaxFiles = DefaultConfig.LogMaxFiles + } + if _, exists := configMap["log_to_journal"]; !exists { + b.LogToJournal = DefaultConfig.LogToJournal + } + if _, exists := configMap["monitor_socket_filename"]; !exists { + b.MonitorSocketFilename = DefaultConfig.MonitorSocketFilename + } + if _, exists := configMap["cups_max_connections"]; !exists { + b.CUPSMaxConnections = DefaultConfig.CUPSMaxConnections + } + if _, exists := configMap["cups_connect_timeout"]; !exists { + b.CUPSConnectTimeout = DefaultConfig.CUPSConnectTimeout + } + if _, exists := configMap["cups_printer_attributes"]; !exists { + b.CUPSPrinterAttributes = DefaultConfig.CUPSPrinterAttributes + } else { + // Make sure all required attributes are present. + s := make(map[string]struct{}, len(b.CUPSPrinterAttributes)) + for _, a := range b.CUPSPrinterAttributes { + s[a] = struct{}{} + } + for _, a := range DefaultConfig.CUPSPrinterAttributes { + if _, exists := s[a]; !exists { + b.CUPSPrinterAttributes = append(b.CUPSPrinterAttributes, a) + } + } + } + if _, exists := configMap["cups_job_full_username"]; !exists { + b.CUPSJobFullUsername = DefaultConfig.CUPSJobFullUsername + } + if _, exists := configMap["cups_ignore_raw_printers"]; !exists { + b.CUPSIgnoreRawPrinters = DefaultConfig.CUPSIgnoreRawPrinters + } + if _, exists := configMap["cups_ignore_class_printers"]; !exists { + b.CUPSIgnoreClassPrinters = DefaultConfig.CUPSIgnoreClassPrinters + } + if _, exists := configMap["copy_printer_info_to_display_name"]; !exists { + b.CUPSCopyPrinterInfoToDisplayName = DefaultConfig.CUPSCopyPrinterInfoToDisplayName + } + + return &b +} + +// Sparse returns a copy of this config with obvious values removed. +func (c *Config) Sparse(context *cli.Context) *Config { + s := *c.commonSparse(context) + + if !context.IsSet("log-file-max-megabytes") && + s.LogFileMaxMegabytes == DefaultConfig.LogFileMaxMegabytes { + s.LogFileMaxMegabytes = 0 + } + if !context.IsSet("log-max-files") && + s.LogMaxFiles == DefaultConfig.LogMaxFiles { + s.LogMaxFiles = 0 + } + if !context.IsSet("log-to-journal") && + reflect.DeepEqual(s.LogToJournal, DefaultConfig.LogToJournal) { + s.LogToJournal = nil + } + if !context.IsSet("monitor-socket-filename") && + s.MonitorSocketFilename == DefaultConfig.MonitorSocketFilename { + s.MonitorSocketFilename = "" + } + if !context.IsSet("cups-max-connections") && + s.CUPSMaxConnections == DefaultConfig.CUPSMaxConnections { + s.CUPSMaxConnections = 0 + } + if !context.IsSet("cups-connect-timeout") && + s.CUPSConnectTimeout == DefaultConfig.CUPSConnectTimeout { + s.CUPSConnectTimeout = "" + } + if reflect.DeepEqual(s.CUPSPrinterAttributes, DefaultConfig.CUPSPrinterAttributes) { + s.CUPSPrinterAttributes = nil + } + if !context.IsSet("cups-job-full-username") && + reflect.DeepEqual(s.CUPSJobFullUsername, DefaultConfig.CUPSJobFullUsername) { + s.CUPSJobFullUsername = nil + } + if !context.IsSet("cups-ignore-raw-printers") && + reflect.DeepEqual(s.CUPSIgnoreRawPrinters, DefaultConfig.CUPSIgnoreRawPrinters) { + s.CUPSIgnoreRawPrinters = nil + } + if !context.IsSet("cups-ignore-class-printers") && + reflect.DeepEqual(s.CUPSIgnoreClassPrinters, DefaultConfig.CUPSIgnoreClassPrinters) { + s.CUPSIgnoreClassPrinters = nil + } + if !context.IsSet("copy-printer-info-to-display-name") && + reflect.DeepEqual(s.CUPSCopyPrinterInfoToDisplayName, DefaultConfig.CUPSCopyPrinterInfoToDisplayName) { + s.CUPSCopyPrinterInfoToDisplayName = nil + } + + return &s +} diff --git a/lib/config_windows.go b/lib/config_windows.go index dfd7805..27dcd25 100644 --- a/lib/config_windows.go +++ b/lib/config_windows.go @@ -12,7 +12,7 @@ import ( "os" "path/filepath" - "github.com/codegangsta/cli" + "github.com/urfave/cli" ) const ( @@ -22,6 +22,15 @@ const ( ) type Config struct { + // Enable local discovery and printing. + LocalPrintingEnable bool `json:"local_printing_enable"` + + // Enable cloud discovery and printing. + CloudPrintingEnable bool `json:"cloud_printing_enable"` + + // Enable fcm notifications instead of xmpp notifications. + FcmNotificationsEnable bool `json:"fcm_notifications_enable"` + // Associated with root account. XMPP credential. XMPPJID string `json:"xmpp_jid,omitempty"` @@ -37,6 +46,9 @@ type Config struct { // User-chosen name of this proxy. Should be unique per Google user account. ProxyName string `json:"proxy_name,omitempty"` + // FCM url client should listen on. + FcmServerBindUrl string `json:"fcm_server_bind_url,omitempty"` + // XMPP server FQDN. XMPPServer string `json:"xmpp_server,omitempty"` @@ -70,31 +82,38 @@ type Config struct { // Maximum quantity of jobs (data) to download concurrently. GCPMaxConcurrentDownloads uint `json:"gcp_max_concurrent_downloads,omitempty"` - // CUPS job queue size. + // Windows Spooler job queue size, must be greater than zero. // TODO: rename without cups_ prefix - NativeJobQueueSize uint `json:"cups_job_queue_size"` + NativeJobQueueSize uint `json:"cups_job_queue_size,omitempty"` - // Interval (eg 10s, 1m) between CUPS printer state polls. + // Interval (eg 10s, 1m) between Windows Spooler printer state polls. // TODO: rename without cups_ prefix - NativePrinterPollInterval string `json:"cups_printer_poll_interval"` + NativePrinterPollInterval string `json:"cups_printer_poll_interval,omitempty"` + + // Use the full username (joe@example.com) in job. + // TODO: rename without cups_ prefix + CUPSJobFullUsername *bool `json:"cups_job_full_username,omitempty"` // Add the job ID to the beginning of the job title. Useful for debugging. - PrefixJobIDToJobTitle bool `json:"prefix_job_id_to_job_title"` + PrefixJobIDToJobTitle *bool `json:"prefix_job_id_to_job_title,omitempty"` // Prefix for all GCP printers hosted by this connector. - DisplayNamePrefix string `json:"display_name_prefix"` + DisplayNamePrefix string `json:"display_name_prefix,omitempty"` // Ignore printers with native names. - PrinterBlacklist []string `json:"printer_blacklist"` + PrinterBlacklist []string `json:"printer_blacklist,omitempty"` - // Enable local discovery and printing. - LocalPrintingEnable bool `json:"local_printing_enable"` - - // Enable cloud discovery and printing. - CloudPrintingEnable bool `json:"cloud_printing_enable"` + // Allow printers with native names. + PrinterWhitelist []string `json:"printer_whitelist,omitempty"` // Least severity to log. LogLevel string `json:"log_level"` + + // Local only: HTTP API port range, low. + LocalPortLow uint16 `json:"local_port_low,omitempty"` + + // Local only: HTTP API port range, high. + LocalPortHigh uint16 `json:"local_port_high,omitempty"` } // DefaultConfig represents reasonable default values for Config fields. @@ -105,6 +124,7 @@ var DefaultConfig = Config{ XMPPPort: 443, XMPPPingTimeout: "5s", XMPPPingInterval: "2m", + FcmServerBindUrl: "https://fcm-stream.googleapis.com/fcm/connect/bind", GCPBaseURL: "https://www.google.com/cloudprint/", GCPOAuthClientID: "539833558011-35iq8btpgas80nrs3o7mv99hm95d4dv6.apps.googleusercontent.com", GCPOAuthClientSecret: "V9BfPOvdiYuw12hDx5Y5nR0a", @@ -114,7 +134,8 @@ var DefaultConfig = Config{ NativeJobQueueSize: 3, NativePrinterPollInterval: "1m", - PrefixJobIDToJobTitle: false, + CUPSJobFullUsername: PointerToBool(false), + PrefixJobIDToJobTitle: PointerToBool(false), DisplayNamePrefix: "", PrinterBlacklist: []string{ "Fax", @@ -122,9 +143,14 @@ var DefaultConfig = Config{ "Microsoft XPS Document Writer", "Google Cloud Printer", }, - LocalPrintingEnable: true, - CloudPrintingEnable: false, - LogLevel: "INFO", + PrinterWhitelist: []string{}, + LocalPrintingEnable: true, + CloudPrintingEnable: false, + FcmNotificationsEnable: false, + LogLevel: "INFO", + + LocalPortLow: 26000, + LocalPortHigh: 26999, } // getConfigFilename gets the absolute filename of the config file specified by @@ -133,7 +159,7 @@ var DefaultConfig = Config{ // If the ConfigFilename exists, then it is returned as an absolute path. // If neither of those exist, the absolute ConfigFilename is returned. func getConfigFilename(context *cli.Context) (string, bool) { - cf := context.GlobalString("config-filename") + cf := context.String("config-filename") if filepath.IsAbs(cf) { // Absolute path specified; user knows what they want. @@ -162,3 +188,13 @@ func getConfigFilename(context *cli.Context) (string, bool) { // This is probably what the user expects if it wasn't found anywhere else. return absCF, false } + +// Backfill returns a copy of this config with all missing keys set to default values. +func (c *Config) Backfill(configMap map[string]interface{}) *Config { + return c.commonBackfill(configMap) +} + +// Sparse returns a copy of this config with obvious values removed. +func (c *Config) Sparse(context *cli.Context) *Config { + return c.commonSparse(context) +} diff --git a/lib/job.go b/lib/job.go index c86f03d..fe34874 100644 --- a/lib/job.go +++ b/lib/job.go @@ -8,7 +8,7 @@ https://developers.google.com/open-source/licenses/bsd package lib -import "github.com/google/cups-connector/cdd" +import "github.com/google/cloud-print-connector/cdd" type Job struct { NativePrinterName string diff --git a/lib/printer.go b/lib/printer.go index 509a939..14218b8 100644 --- a/lib/printer.go +++ b/lib/printer.go @@ -9,36 +9,39 @@ https://developers.google.com/open-source/licenses/bsd package lib import ( - "bytes" - "encoding/json" "reflect" "regexp" - "github.com/google/cups-connector/cdd" + "github.com/google/cloud-print-connector/cdd" ) type PrinterState uint8 +// DuplexVendorMap maps a DuplexType to a CUPS key:value option string for a given printer. +type DuplexVendorMap map[cdd.DuplexType]string + // CUPS: cups_dest_t; GCP: /register and /update interfaces type Printer struct { - GCPID string // GCP: printerid (GCP key) - Name string // CUPS: cups_dest_t.name (CUPS key); GCP: name field - DefaultDisplayName string // CUPS: printer-info; GCP: default_display_name field - UUID string // CUPS: printer-uuid; GCP: uuid field - Manufacturer string // CUPS: PPD; GCP: manufacturer field - Model string // CUPS: PPD; GCP: model field - GCPVersion string // GCP: gcpVersion field - SetupURL string // GCP: setup_url field - SupportURL string // GCP: support_url field - UpdateURL string // GCP: update_url field - ConnectorVersion string // GCP: firmware field - State *cdd.PrinterStateSection // CUPS: various; GCP: semantic_state field - Description *cdd.PrinterDescriptionSection // CUPS: translated PPD; GCP: capabilities field - CapsHash string // CUPS: hash of PPD; GCP: capsHash field - Tags map[string]string // CUPS: all printer attributes; GCP: repeated tag field - NativeJobSemaphore *Semaphore - QuotaEnabled bool - DailyQuota int + GCPID string // GCP: printerid (GCP key) + Name string // CUPS: cups_dest_t.name (CUPS key); GCP: name field + DefaultDisplayName string // CUPS: printer-info; GCP: default_display_name field + UUID string // CUPS: printer-uuid; GCP: uuid field + Manufacturer string // CUPS: PPD; GCP: manufacturer field + Model string // CUPS: PPD; GCP: model field + GCPVersion string // GCP: gcpVersion field + SetupURL string // GCP: setup_url field + SupportURL string // GCP: support_url field + UpdateURL string // GCP: update_url field + ConnectorVersion string // GCP: firmware field + State *cdd.PrinterStateSection // CUPS: various; GCP: semantic_state field + Description *cdd.PrinterDescriptionSection // CUPS: translated PPD; GCP: capabilities field + CapsHash string // CUPS: hash of PPD; GCP: capsHash field + Tags map[string]string // CUPS: all printer attributes; GCP: repeated tag field + DuplexMap DuplexVendorMap // CUPS: PPD; + NativeJobSemaphore *Semaphore + QuotaEnabled bool + DailyQuota int + NotificationChannel string } var rDeviceURIHostname *regexp.Regexp = regexp.MustCompile( @@ -73,20 +76,22 @@ type PrinterDiff struct { Operation PrinterDiffOperation Printer Printer - DefaultDisplayNameChanged bool - ManufacturerChanged bool - ModelChanged bool - GCPVersionChanged bool - SetupURLChanged bool - SupportURLChanged bool - UpdateURLChanged bool - ConnectorVersionChanged bool - StateChanged bool - DescriptionChanged bool - CapsHashChanged bool - TagsChanged bool - QuotaEnabledChanged bool - DailyQuotaChanged bool + DefaultDisplayNameChanged bool + ManufacturerChanged bool + ModelChanged bool + GCPVersionChanged bool + SetupURLChanged bool + SupportURLChanged bool + UpdateURLChanged bool + ConnectorVersionChanged bool + StateChanged bool + DescriptionChanged bool + CapsHashChanged bool + TagsChanged bool + DuplexMapChanged bool + QuotaEnabledChanged bool + DailyQuotaChanged bool + NotificationChannelChanged bool } func printerSliceToMapByName(s []Printer) map[string]Printer { @@ -191,11 +196,7 @@ func diffPrinter(pn, pg *Printer) PrinterDiff { if !reflect.DeepEqual(pg.State, pn.State) { d.StateChanged = true } - // PrinterDescriptionSection objects contain fields that are not exported to JSON, - // and therefore cause comparison with GCP's copy to incorrectly appear not equal. - pgDescJSON, _ := json.Marshal(pg.Description) - pnDescJSON, _ := json.Marshal(pn.Description) - if !bytes.Equal(pgDescJSON, pnDescJSON) { + if !reflect.DeepEqual(pg.Description, pn.Description) { d.DescriptionChanged = true } if pg.CapsHash != pn.CapsHash { @@ -208,6 +209,10 @@ func diffPrinter(pn, pg *Printer) PrinterDiff { d.TagsChanged = true } + if !reflect.DeepEqual(pg.DuplexMap, pn.DuplexMap) { + d.DuplexMapChanged = true + } + if pg.QuotaEnabled != pn.QuotaEnabled { d.QuotaEnabledChanged = true } @@ -216,11 +221,16 @@ func diffPrinter(pn, pg *Printer) PrinterDiff { d.DailyQuotaChanged = true } + if pg.NotificationChannel != pn.NotificationChannel { + d.NotificationChannelChanged = true + } + if d.DefaultDisplayNameChanged || d.ManufacturerChanged || d.ModelChanged || - d.GCPVersionChanged || d.SetupURLChanged || d.SupportURLChanged || - d.UpdateURLChanged || d.ConnectorVersionChanged || d.StateChanged || - d.DescriptionChanged || d.CapsHashChanged || d.TagsChanged || - d.QuotaEnabledChanged || d.DailyQuotaChanged { + d.GCPVersionChanged || d.SetupURLChanged || d.SupportURLChanged || + d.UpdateURLChanged || d.ConnectorVersionChanged || d.StateChanged || + d.DescriptionChanged || d.CapsHashChanged || d.TagsChanged || + d.DuplexMapChanged || d.QuotaEnabledChanged || d.DailyQuotaChanged || + d.NotificationChannelChanged { return d } diff --git a/log/log_unix.go b/log/log_unix.go index 5211b73..7b759ad 100644 --- a/log/log_unix.go +++ b/log/log_unix.go @@ -4,7 +4,7 @@ // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd -// +build linux darwin +// +build linux darwin freebsd // The log package logs to an io.Writer using the same log format that CUPS uses. package log diff --git a/log/log_windows.go b/log/log_windows.go index 3d28670..8194030 100644 --- a/log/log_windows.go +++ b/log/log_windows.go @@ -12,7 +12,7 @@ package log import ( "fmt" - "github.com/google/cups-connector/lib" + "github.com/google/cloud-print-connector/lib" "golang.org/x/sys/windows/svc/debug" "golang.org/x/sys/windows/svc/eventlog" ) diff --git a/log/logroller.go b/log/logroller.go index 972ee31..fe45af8 100644 --- a/log/logroller.go +++ b/log/logroller.go @@ -4,7 +4,7 @@ // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd -// +build linux darwin +// +build linux darwin freebsd package log diff --git a/manager/printermanager.go b/manager/printermanager.go index 5b842c8..a711464 100644 --- a/manager/printermanager.go +++ b/manager/printermanager.go @@ -17,18 +17,20 @@ import ( "sync" "time" - "github.com/google/cups-connector/cdd" - "github.com/google/cups-connector/gcp" - "github.com/google/cups-connector/lib" - "github.com/google/cups-connector/log" - "github.com/google/cups-connector/privet" - "github.com/google/cups-connector/xmpp" + "github.com/google/cloud-print-connector/cdd" + "github.com/google/cloud-print-connector/gcp" + "github.com/google/cloud-print-connector/lib" + "github.com/google/cloud-print-connector/log" + "github.com/google/cloud-print-connector/notification" + "github.com/google/cloud-print-connector/privet" + "github.com/google/cloud-print-connector/xmpp" ) type NativePrintSystem interface { GetPrinters() ([]lib.Printer, error) GetJobState(printerName string, jobID uint32) (*cdd.PrintJobStateDiff, error) Print(printer *lib.Printer, fileName, title, user, gcpJobID string, ticket *cdd.CloudJobTicket) (uint32, error) + ReleaseJob(printerName string, jobID uint32) error RemoveCachedPPD(printerName string) } @@ -55,10 +57,11 @@ type PrinterManager struct { jobFullUsername bool shareScope string - quit chan struct{} + quit chan struct{} + useFcm bool } -func NewPrinterManager(native NativePrintSystem, gcp *gcp.GoogleCloudPrint, privet *privet.Privet, printerPollInterval time.Duration, nativeJobQueueSize uint, jobFullUsername bool, shareScope string, jobs <-chan *lib.Job, xmppNotifications <-chan xmpp.PrinterNotification) (*PrinterManager, error) { +func NewPrinterManager(native NativePrintSystem, gcp *gcp.GoogleCloudPrint, privet *privet.Privet, printerPollInterval time.Duration, nativeJobQueueSize uint, jobFullUsername bool, shareScope string, jobs <-chan *lib.Job, notifications <-chan notification.PrinterNotification, useFcm bool) (*PrinterManager, error) { var printers *lib.ConcurrentPrinterMap var queuedJobsCount map[string]uint @@ -98,13 +101,14 @@ func NewPrinterManager(native NativePrintSystem, gcp *gcp.GoogleCloudPrint, priv jobFullUsername: jobFullUsername, shareScope: shareScope, - quit: make(chan struct{}), + quit: make(chan struct{}), + useFcm: useFcm, } // Sync once before returning, to make sure things are working. // Ignore privet updates this first time because Privet always starts // with zero printers. - if err = pm.syncPrinters(true); err != nil { + if err = pm.SyncPrinters(true); err != nil { return nil, err } @@ -121,7 +125,7 @@ func NewPrinterManager(native NativePrintSystem, gcp *gcp.GoogleCloudPrint, priv } pm.syncPrintersPeriodically(printerPollInterval) - pm.listenNotifications(jobs, xmppNotifications) + pm.listenNotifications(jobs, notifications) if gcp != nil { for gcpPrinterID := range queuedJobsCount { @@ -145,7 +149,7 @@ func (pm *PrinterManager) syncPrintersPeriodically(interval time.Duration) { for { select { case <-t.C: - if err := pm.syncPrinters(false); err != nil { + if err := pm.SyncPrinters(false); err != nil { log.Error(err) } t.Reset(interval) @@ -157,8 +161,8 @@ func (pm *PrinterManager) syncPrintersPeriodically(interval time.Duration) { }() } -func (pm *PrinterManager) syncPrinters(ignorePrivet bool) error { - log.Info("Synchronizing printers, stand by") +func (pm *PrinterManager) SyncPrinters(ignorePrivet bool) error { + log.Debug("Synchronizing printers, stand by") // Get current snapshot of native printers. nativePrinters, err := pm.native.GetPrinters() @@ -175,12 +179,18 @@ func (pm *PrinterManager) syncPrinters(ignorePrivet bool) error { h = adler32.New() lib.DeepHash(nativePrinters[i].Description, h) nativePrinters[i].CapsHash = fmt.Sprintf("%x", h.Sum(nil)) + + if pm.useFcm { + nativePrinters[i].NotificationChannel = gcp.FCP_CHANNEL + } else { + nativePrinters[i].NotificationChannel = gcp.XMPP_CHANNEL + } } // Compare the snapshot to what we know currently. diffs := lib.DiffPrinters(nativePrinters, pm.printers.GetAll()) if diffs == nil { - log.Infof("Printers are already in sync; there are %d", len(nativePrinters)) + log.Debugf("Printers are already in sync; there are %d", len(nativePrinters)) return nil } @@ -199,7 +209,7 @@ func (pm *PrinterManager) syncPrinters(ignorePrivet bool) error { // Update what we know. pm.printers.Refresh(currentPrinters) - log.Infof("Finished synchronizing %d printers", len(currentPrinters)) + log.Debugf("Finished synchronizing %d printers", len(currentPrinters)) return nil } @@ -287,7 +297,7 @@ func (pm *PrinterManager) applyDiff(diff *lib.PrinterDiff, ch chan<- lib.Printer } // listenNotifications handles the messages found on the channels. -func (pm *PrinterManager) listenNotifications(jobs <-chan *lib.Job, xmppMessages <-chan xmpp.PrinterNotification) { +func (pm *PrinterManager) listenNotifications(jobs <-chan *lib.Job, messages <-chan notification.PrinterNotification) { go func() { for { select { @@ -298,10 +308,10 @@ func (pm *PrinterManager) listenNotifications(jobs <-chan *lib.Job, xmppMessages log.DebugJobf(job.JobID, "Received job: %+v", job) go pm.printJob(job.NativePrinterName, job.Filename, job.Title, job.User, job.JobID, job.Ticket, job.UpdateJob) - case notification := <-xmppMessages: - log.Debugf("Received XMPP message: %+v", notification) - if notification.Type == xmpp.PrinterNewJobs { - if p, exists := pm.printers.GetByGCPID(notification.GCPID); exists { + case message := <-messages: + log.Debugf("Received message: %+v", message) + if message.Type == notification.PrinterNewJobs { + if p, exists := pm.printers.GetByGCPID(message.GCPID); exists { go pm.gcp.HandleJobs(&p, func() { pm.incrementJobsProcessed(false) }) } } @@ -401,6 +411,7 @@ func (pm *PrinterManager) printJob(nativePrinterName, filename, title, user, job ticker := time.NewTicker(time.Second) defer ticker.Stop() + defer pm.releaseJob(printer.Name, nativeJobID, jobID) for _ = range ticker.C { nativeState, err := pm.native.GetJobState(printer.Name, nativeJobID) @@ -440,6 +451,12 @@ func (pm *PrinterManager) printJob(nativePrinterName, filename, title, user, job } } +func (pm *PrinterManager) releaseJob(printerName string, nativeJobID uint32, jobID string) { + if err := pm.native.ReleaseJob(printerName, nativeJobID); err != nil { + log.ErrorJob(jobID, err) + } +} + // GetJobStats returns information that is useful for monitoring // the connector. func (pm *PrinterManager) GetJobStats() (uint, uint, uint, error) { diff --git a/monitor/monitor.go b/monitor/monitor.go index bad9ede..49f86b0 100644 --- a/monitor/monitor.go +++ b/monitor/monitor.go @@ -4,7 +4,7 @@ // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd -// +build linux darwin +// +build linux darwin freebsd package monitor @@ -12,12 +12,12 @@ import ( "fmt" "net" - "github.com/google/cups-connector/cups" - "github.com/google/cups-connector/gcp" - "github.com/google/cups-connector/lib" - "github.com/google/cups-connector/log" - "github.com/google/cups-connector/manager" - "github.com/google/cups-connector/privet" + "github.com/google/cloud-print-connector/cups" + "github.com/google/cloud-print-connector/gcp" + "github.com/google/cloud-print-connector/lib" + "github.com/google/cloud-print-connector/log" + "github.com/google/cloud-print-connector/manager" + "github.com/google/cloud-print-connector/privet" ) const monitorFormat = `cups-printers=%d diff --git a/notification/notification.go b/notification/notification.go new file mode 100644 index 0000000..90a8eee --- /dev/null +++ b/notification/notification.go @@ -0,0 +1,12 @@ +package notification + +type PrinterNotificationType uint8 +type PrinterNotification struct { + GCPID string + Type PrinterNotificationType +} + +const ( + PrinterNewJobs PrinterNotificationType = iota + PrinterDelete +) diff --git a/privet/api-server.go b/privet/api-server.go index ec6ff55..f10aa6d 100644 --- a/privet/api-server.go +++ b/privet/api-server.go @@ -11,7 +11,6 @@ package privet import ( "encoding/json" "errors" - "fmt" "io" "io/ioutil" "net" @@ -21,9 +20,9 @@ import ( "strings" "time" - "github.com/google/cups-connector/cdd" - "github.com/google/cups-connector/lib" - "github.com/google/cups-connector/log" + "github.com/google/cloud-print-connector/cdd" + "github.com/google/cloud-print-connector/lib" + "github.com/google/cloud-print-connector/log" ) var ( @@ -85,11 +84,7 @@ type privetAPI struct { startTime time.Time } -func newPrivetAPI(gcpID, name, gcpBaseURL string, xsrf xsrfSecret, online bool, jc *jobCache, jobs chan<- *lib.Job, getPrinter func(string) (lib.Printer, bool), getProximityToken func(string, string) ([]byte, int, error)) (*privetAPI, error) { - l, err := newQuittableListener() - if err != nil { - return nil, err - } +func newPrivetAPI(gcpID, name, gcpBaseURL string, xsrf xsrfSecret, online bool, jc *jobCache, jobs chan<- *lib.Job, getPrinter func(string) (lib.Printer, bool), getProximityToken func(string, string) ([]byte, int, error), listener *quittableListener) (*privetAPI, error) { api := &privetAPI{ gcpID: gcpID, name: name, @@ -102,7 +97,7 @@ func newPrivetAPI(gcpID, name, gcpBaseURL string, xsrf xsrfSecret, online bool, getPrinter: getPrinter, getProximityToken: getProximityToken, - listener: l, + listener: listener, startTime: time.Now(), } go api.serve() @@ -138,7 +133,7 @@ func (api *privetAPI) serve() { type infoResponse struct { Version string `json:"version"` Name string `json:"name"` - Description string `json:"description"` + Description string `json:"description,omitempty"` URL string `json:"url"` Type []string `json:"type"` ID string `json:"id"` @@ -146,15 +141,15 @@ type infoResponse struct { ConnectionState string `json:"connection_state"` Manufacturer string `json:"manufacturer"` Model string `json:"model"` - SerialNumber string `json:"serial_number"` + SerialNumber string `json:"serial_number,omitempty"` Firmware string `json:"firmware"` Uptime uint `json:"uptime"` - SetupURL string `json:"setup_url"` - SupportURL string `json:"support_url"` - UpdateURL string `json:"update_url"` + SetupURL string `json:"setup_url,omitempty"` + SupportURL string `json:"support_url,omitempty"` + UpdateURL string `json:"update_url,omitempty"` XPrivetToken string `json:"x-privet-token"` API []string `json:"api"` - SemanticState cdd.CloudDeviceState `json:"semantic_state"` + SemanticState cdd.CloudDeviceState `json:"semantic_state,omitempty"` } func (api *privetAPI) info(w http.ResponseWriter, r *http.Request) { @@ -386,7 +381,7 @@ func (api *privetAPI) submitdoc(w http.ResponseWriter, r *http.Request) { return } - file, err := ioutil.TempFile("", "cups-connector-privet-") + file, err := ioutil.TempFile("", "cloud-print-connector-privet-") if err != nil { log.Errorf("Failed to create file for new Privet job: %s", err) w.WriteHeader(http.StatusInternalServerError) @@ -494,43 +489,3 @@ func (api *privetAPI) jobstate(w http.ResponseWriter, r *http.Request) { w.Write(jobState) } - -type quittableListener struct { - *net.TCPListener - // When q is closed, the listener is quitting. - q chan struct{} -} - -func newQuittableListener() (*quittableListener, error) { - l, err := net.ListenTCP("tcp", nil) - if err != nil { - return nil, fmt.Errorf("Failed to start Privet API listener: %s", err) - } - return &quittableListener{l, make(chan struct{}, 0)}, nil -} - -func (l *quittableListener) Accept() (net.Conn, error) { - conn, err := l.AcceptTCP() - - select { - case <-l.q: - if err == nil { - conn.Close() - } - // The listener was closed on purpose. - // Returning an error that is not a net.Error causes net.Server.Serve() to return. - return nil, closed - default: - } - - // Clean up zombie connections. - conn.SetKeepAlive(true) - conn.SetKeepAlivePeriod(time.Minute) - - return conn, err -} - -func (l *quittableListener) quit() { - close(l.q) - l.Close() -} diff --git a/privet/avahi.c b/privet/avahi.c index c399881..c29924c 100644 --- a/privet/avahi.c +++ b/privet/avahi.c @@ -4,7 +4,7 @@ // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd -// +build linux +// +build linux freebsd #include "avahi.h" #include "_cgo_export.h" @@ -36,38 +36,47 @@ const char *startAvahiClient(AvahiThreadedPoll **threaded_poll, AvahiClient **cl return NULL; } -const char *addAvahiGroup(AvahiThreadedPoll *threaded_poll, AvahiClient *client, - AvahiEntryGroup **group, const char *service_name, unsigned short port, AvahiStringList *txt) { - *group = avahi_entry_group_new(client, handleGroupStateChange, (void *)service_name); - if (!*group) { - return avahi_strerror(avahi_client_errno(client)); - } - +static const char *populateGroup(AvahiClient *client, AvahiEntryGroup *group, + const char *service_name, unsigned short port, AvahiStringList *txt) { int error = avahi_entry_group_add_service_strlst( - *group, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, 0, service_name, + group, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, 0, service_name, SERVICE_TYPE, NULL, NULL, port, txt); if (AVAHI_OK != error) { - avahi_entry_group_free(*group); + avahi_entry_group_free(group); return avahi_strerror(error); } - error = avahi_entry_group_add_service_subtype(*group, AVAHI_IF_UNSPEC, + error = avahi_entry_group_add_service_subtype(group, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, 0, service_name, SERVICE_TYPE, NULL, SERVICE_SUBTYPE); if (AVAHI_OK != error) { - avahi_entry_group_free(*group); + avahi_entry_group_free(group); return avahi_strerror(error); } - error = avahi_entry_group_commit(*group); + error = avahi_entry_group_commit(group); if (AVAHI_OK != error) { - avahi_entry_group_free(*group); + avahi_entry_group_free(group); return avahi_strerror(error); } return NULL; } -const char *updateAvahiGroup(AvahiThreadedPoll *threaded_poll, AvahiEntryGroup *group, - const char *service_name, AvahiStringList *txt) { +const char *addAvahiGroup(AvahiClient *client, AvahiEntryGroup **group, const char *printer_name, + const char *service_name, unsigned short port, AvahiStringList *txt) { + *group = avahi_entry_group_new(client, handleGroupStateChange, (void *)printer_name); + if (!*group) { + return avahi_strerror(avahi_client_errno(client)); + } + return populateGroup(client, *group, service_name, port, txt); +} + +const char *resetAvahiGroup(AvahiClient *client, AvahiEntryGroup *group, const char *service_name, + unsigned short port, AvahiStringList *txt) { + avahi_entry_group_reset(group); + return populateGroup(client, group, service_name, port, txt); +} + +const char *updateAvahiGroup(AvahiEntryGroup *group, const char *service_name, AvahiStringList *txt) { int error = avahi_entry_group_update_service_txt_strlst(group, AVAHI_IF_UNSPEC, AVAHI_PROTO_UNSPEC, 0, service_name, SERVICE_TYPE, NULL, txt); if (AVAHI_OK != error) { @@ -76,7 +85,7 @@ const char *updateAvahiGroup(AvahiThreadedPoll *threaded_poll, AvahiEntryGroup * return NULL; } -const char *removeAvahiGroup(AvahiThreadedPoll *threaded_poll, AvahiEntryGroup *group) { +const char *removeAvahiGroup(AvahiEntryGroup *group) { int error = avahi_entry_group_free(group); if (AVAHI_OK != error) { return avahi_strerror(error); diff --git a/privet/avahi.go b/privet/avahi.go index 6dc741d..6195d52 100644 --- a/privet/avahi.go +++ b/privet/avahi.go @@ -4,11 +4,13 @@ // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd -// +build linux +// +build linux freebsd package privet -// #cgo LDFLAGS: -lavahi-client -lavahi-common +// #cgo linux LDFLAGS: -lavahi-client -lavahi-common +// #cgo freebsd CFLAGS: -I/usr/local/include +// #cgo freebsd LDFLAGS: -L/usr/local/lib -lavahi-client -lavahi-common // #include "avahi.h" import "C" import ( @@ -17,13 +19,14 @@ import ( "sync" "unsafe" - "github.com/google/cups-connector/log" + "github.com/google/cloud-print-connector/log" ) var ( txtversKey = C.CString("txtvers") txtversValue = C.CString("1") tyKey = C.CString("ty") + noteKey = C.CString("note") urlKey = C.CString("url") typeKey = C.CString("type") typeValue = C.CString("printer") @@ -34,15 +37,19 @@ var ( ) type record struct { - // name is the name of the service, which must live on the heap so that the + // printerName is the name of the printer, which must live on the heap so that the // C event handler can see it. - name *C.char - port uint16 - ty string - url string - id string - online bool - group *C.AvahiEntryGroup + // serviceName is the name of the service, which is the same as name except + // when there is a collision. + printerName *C.char + serviceName *C.char + port uint16 + ty string + note string + url string + id string + online bool + group *C.AvahiEntryGroup } type zeroconf struct { @@ -79,7 +86,7 @@ func newZeroconf() (*zeroconf, error) { return &z, nil } -func prepareTXT(ty, url, id string, online bool) *C.AvahiStringList { +func prepareTXT(ty, note, url, id string, online bool) *C.AvahiStringList { var txt *C.AvahiStringList txt = C.avahi_string_list_add_pair(txt, txtversKey, txtversValue) txt = C.avahi_string_list_add_pair(txt, typeKey, typeValue) @@ -88,6 +95,12 @@ func prepareTXT(ty, url, id string, online bool) *C.AvahiStringList { defer C.free(unsafe.Pointer(tyValue)) txt = C.avahi_string_list_add_pair(txt, tyKey, tyValue) + if note != "" { + noteValue := C.CString(note) + defer C.free(unsafe.Pointer(noteValue)) + txt = C.avahi_string_list_add_pair(txt, noteKey, noteValue) + } + urlValue := C.CString(url) defer C.free(unsafe.Pointer(urlValue)) txt = C.avahi_string_list_add_pair(txt, urlKey, urlValue) @@ -105,30 +118,37 @@ func prepareTXT(ty, url, id string, online bool) *C.AvahiStringList { return txt } -func (z *zeroconf) addPrinter(name string, port uint16, ty, url, id string, online bool) error { +func (z *zeroconf) addPrinter(name string, port uint16, ty, note, url, id string, online bool) error { + nameValue := C.CString(name) r := record{ - name: C.CString(name), - port: port, - ty: ty, - url: url, - id: id, - online: online, + printerName: nameValue, + serviceName: C.avahi_strdup(nameValue), + port: port, + ty: ty, + note: note, + url: url, + id: id, + online: online, } z.spMutex.Lock() defer z.spMutex.Unlock() if _, exists := z.printers[name]; exists { + C.free(unsafe.Pointer(r.printerName)) + C.avahi_free(unsafe.Pointer(r.serviceName)) return fmt.Errorf("printer %s was already added to Avahi publishing", name) } if z.state == C.AVAHI_CLIENT_S_RUNNING { - txt := prepareTXT(ty, url, id, online) + txt := prepareTXT(ty, note, url, id, online) defer C.avahi_string_list_free(txt) C.avahi_threaded_poll_lock(z.threadedPoll) defer C.avahi_threaded_poll_unlock(z.threadedPoll) - if errstr := C.addAvahiGroup(z.threadedPoll, z.client, &r.group, r.name, C.ushort(r.port), txt); errstr != nil { + if errstr := C.addAvahiGroup(z.client, &r.group, r.printerName, r.serviceName, C.ushort(r.port), txt); errstr != nil { + C.free(unsafe.Pointer(r.printerName)) + C.avahi_free(unsafe.Pointer(r.serviceName)) err := fmt.Errorf("Failed to add Avahi group: %s", C.GoString(errstr)) return err } @@ -138,7 +158,7 @@ func (z *zeroconf) addPrinter(name string, port uint16, ty, url, id string, onli return nil } -func (z *zeroconf) updatePrinterTXT(name, ty, url, id string, online bool) error { +func (z *zeroconf) updatePrinterTXT(name, ty, note, url, id string, online bool) error { z.spMutex.Lock() defer z.spMutex.Unlock() @@ -153,13 +173,13 @@ func (z *zeroconf) updatePrinterTXT(name, ty, url, id string, online bool) error r.online = online if z.state == C.AVAHI_CLIENT_S_RUNNING && r.group != nil { - txt := prepareTXT(ty, url, id, online) + txt := prepareTXT(ty, note, url, id, online) defer C.avahi_string_list_free(txt) C.avahi_threaded_poll_lock(z.threadedPoll) defer C.avahi_threaded_poll_unlock(z.threadedPoll) - if errstr := C.updateAvahiGroup(z.threadedPoll, r.group, r.name, txt); errstr != nil { + if errstr := C.updateAvahiGroup(r.group, r.serviceName, txt); errstr != nil { err := fmt.Errorf("Failed to update Avahi group: %s", C.GoString(errstr)) return err } @@ -182,13 +202,14 @@ func (z *zeroconf) removePrinter(name string) error { C.avahi_threaded_poll_lock(z.threadedPoll) defer C.avahi_threaded_poll_unlock(z.threadedPoll) - if errstr := C.removeAvahiGroup(z.threadedPoll, r.group); errstr != nil { + if errstr := C.removeAvahiGroup(r.group); errstr != nil { err := fmt.Errorf("Failed to remove Avahi group: %s", C.GoString(errstr)) return err } } - C.free(unsafe.Pointer(r.name)) + C.free(unsafe.Pointer(r.printerName)) + C.avahi_free(unsafe.Pointer(r.serviceName)) delete(z.printers, name) return nil @@ -246,7 +267,7 @@ func handleClientStateChange(client *C.AvahiClient, newState C.AvahiClientState, log.Info("Local printing disabled (Avahi client is not running).") for name, r := range z.printers { if r.group != nil { - if errstr := C.removeAvahiGroup(z.threadedPoll, r.group); errstr != nil { + if errstr := C.removeAvahiGroup(r.group); errstr != nil { err := errors.New(C.GoString(errstr)) log.Errorf("Failed to remove Avahi group: %s", err) } @@ -260,10 +281,10 @@ func handleClientStateChange(client *C.AvahiClient, newState C.AvahiClientState, if newState == C.AVAHI_CLIENT_S_RUNNING { log.Info("Local printing enabled (Avahi client is running).") for name, r := range z.printers { - txt := prepareTXT(r.ty, r.url, r.id, r.online) + txt := prepareTXT(r.ty, r.note, r.url, r.id, r.online) defer C.avahi_string_list_free(txt) - if errstr := C.addAvahiGroup(z.threadedPoll, z.client, &r.group, r.name, C.ushort(r.port), txt); errstr != nil { + if errstr := C.addAvahiGroup(z.client, &r.group, r.printerName, r.serviceName, C.ushort(r.port), txt); errstr != nil { err := errors.New(C.GoString(errstr)) log.Errorf("Failed to add Avahi group: %s", err) } @@ -284,8 +305,27 @@ func handleClientStateChange(client *C.AvahiClient, newState C.AvahiClientState, func handleGroupStateChange(group *C.AvahiEntryGroup, state C.AvahiEntryGroupState, name unsafe.Pointer) { switch state { case C.AVAHI_ENTRY_GROUP_COLLISION: - log.Warningf("Avahi failed to register %s due to a naming collision", C.GoString((*C.char)(name))) + z := instance + z.spMutex.Lock() + defer z.spMutex.Unlock() + + // Pick a new name. + printerName := C.GoString((*C.char)(name)) + r := z.printers[printerName] + txt := prepareTXT(r.ty, r.note, r.url, r.id, r.online) + defer C.avahi_string_list_free(txt) + altName := C.avahi_alternative_service_name(r.serviceName) + C.avahi_free(unsafe.Pointer(r.serviceName)) + r.serviceName = altName + log.Warningf("Avahi failed to register '%s' due to a naming collision, trying with '%s'", printerName, C.GoString((*C.char)(altName))) + if errstr := C.resetAvahiGroup(z.client, r.group, r.serviceName, C.ushort(r.port), txt); errstr != nil { + r.group = nil + err := errors.New(C.GoString(errstr)) + log.Errorf("Failed to reset Avahi group: %s", err) + } + z.printers[printerName] = r + case C.AVAHI_ENTRY_GROUP_FAILURE: - log.Warningf("Avahi failed to register %s, don't know why", C.GoString((*C.char)(name))) + log.Warningf("Avahi failed to register '%s', don't know why", C.GoString((*C.char)(name))) } } diff --git a/privet/avahi.h b/privet/avahi.h index c8aa622..5158407 100644 --- a/privet/avahi.h +++ b/privet/avahi.h @@ -4,19 +4,22 @@ // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd -// +build linux +// +build linux freebsd #include +#include #include +#include #include #include #include // free const char *startAvahiClient(AvahiThreadedPoll **threaded_poll, AvahiClient **client); -const char *addAvahiGroup(AvahiThreadedPoll *threaded_poll, AvahiClient *client, - AvahiEntryGroup **group, const char *serviceName, unsigned short port, AvahiStringList *txt); -const char *updateAvahiGroup(AvahiThreadedPoll *threaded_poll, AvahiEntryGroup *group, - const char *serviceName, AvahiStringList *txt); -const char *removeAvahiGroup(AvahiThreadedPoll *threaded_poll, AvahiEntryGroup *group); +const char *addAvahiGroup(AvahiClient *client, AvahiEntryGroup **group, const char *printer_name, + const char *service_name, unsigned short port, AvahiStringList *txt); +const char *resetAvahiGroup(AvahiClient *client, AvahiEntryGroup *group, const char *service_name, + unsigned short port, AvahiStringList *txt); +const char *updateAvahiGroup(AvahiEntryGroup *group, const char *service_name, AvahiStringList *txt); +const char *removeAvahiGroup(AvahiEntryGroup *group); void stopAvahiClient(AvahiThreadedPoll *threaded_poll, AvahiClient *client); diff --git a/privet/bonjour.c b/privet/bonjour.c index 39b0c06..65b4080 100644 --- a/privet/bonjour.c +++ b/privet/bonjour.c @@ -51,10 +51,11 @@ void registerCallback(CFNetServiceRef service, CFStreamError *streamError, void // startBonjour starts and returns a bonjour service. // // Returns a registered service. Returns NULL and sets err on failure. -CFNetServiceRef startBonjour(const char *name, const char *type, unsigned short int port, const char *ty, const char *url, const char *id, const char *cs, char **err) { +CFNetServiceRef startBonjour(const char *name, const char *type, unsigned short int port, const char *ty, const char *note, const char *url, const char *id, const char *cs, char **err) { CFStringRef nameCF = CFStringCreateWithCString(NULL, name, kCFStringEncodingASCII); CFStringRef typeCF = CFStringCreateWithCString(NULL, type, kCFStringEncodingASCII); CFStringRef tyCF = CFStringCreateWithCString(NULL, ty, kCFStringEncodingASCII); + CFStringRef noteCF = CFStringCreateWithCString(NULL, note, kCFStringEncodingASCII); CFStringRef urlCF = CFStringCreateWithCString(NULL, url, kCFStringEncodingASCII); CFStringRef idCF = CFStringCreateWithCString(NULL, id, kCFStringEncodingASCII); CFStringRef csCF = CFStringCreateWithCString(NULL, cs, kCFStringEncodingASCII); @@ -63,6 +64,9 @@ CFNetServiceRef startBonjour(const char *name, const char *type, unsigned short &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); CFDictionarySetValue(dict, CFSTR("txtvers"), CFSTR("1")); CFDictionarySetValue(dict, CFSTR("ty"), tyCF); + if (CFStringGetLength(noteCF) > 0) { + CFDictionarySetValue(dict, CFSTR("note"), noteCF); + } CFDictionarySetValue(dict, CFSTR("url"), urlCF); CFDictionarySetValue(dict, CFSTR("type"), CFSTR("printer")); CFDictionarySetValue(dict, CFSTR("id"), idCF); @@ -89,6 +93,7 @@ CFNetServiceRef startBonjour(const char *name, const char *type, unsigned short CFRelease(typeCF); CFRelease(tyCF); + CFRelease(noteCF); CFRelease(urlCF); CFRelease(idCF); CFRelease(csCF); @@ -99,8 +104,9 @@ CFNetServiceRef startBonjour(const char *name, const char *type, unsigned short } // updateBonjour updates the TXT record of service. -void updateBonjour(CFNetServiceRef service, const char *ty, const char *url, const char *id, const char *cs) { +void updateBonjour(CFNetServiceRef service, const char *ty, const char *note, const char *url, const char *id, const char *cs) { CFStringRef tyCF = CFStringCreateWithCString(NULL, ty, kCFStringEncodingASCII); + CFStringRef noteCF = CFStringCreateWithCString(NULL, note, kCFStringEncodingASCII); CFStringRef urlCF = CFStringCreateWithCString(NULL, url, kCFStringEncodingASCII); CFStringRef idCF = CFStringCreateWithCString(NULL, id, kCFStringEncodingASCII); CFStringRef csCF = CFStringCreateWithCString(NULL, cs, kCFStringEncodingASCII); @@ -109,6 +115,9 @@ void updateBonjour(CFNetServiceRef service, const char *ty, const char *url, con &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); CFDictionarySetValue(dict, CFSTR("txtvers"), CFSTR("1")); CFDictionarySetValue(dict, CFSTR("ty"), tyCF); + if (CFStringGetLength(noteCF) > 0) { + CFDictionarySetValue(dict, CFSTR("note"), noteCF); + } CFDictionarySetValue(dict, CFSTR("url"), urlCF); CFDictionarySetValue(dict, CFSTR("type"), CFSTR("printer")); CFDictionarySetValue(dict, CFSTR("id"), idCF); @@ -118,6 +127,7 @@ void updateBonjour(CFNetServiceRef service, const char *ty, const char *url, con CFNetServiceSetTXTData(service, txt); CFRelease(tyCF); + CFRelease(noteCF); CFRelease(urlCF); CFRelease(idCF); CFRelease(csCF); diff --git a/privet/bonjour.go b/privet/bonjour.go index bfc985e..bc64625 100644 --- a/privet/bonjour.go +++ b/privet/bonjour.go @@ -17,7 +17,7 @@ import ( "sync" "unsafe" - "github.com/google/cups-connector/log" + "github.com/google/cloud-print-connector/log" ) // TODO: How to add the _printer subtype? @@ -38,7 +38,7 @@ func newZeroconf() (*zeroconf, error) { return &z, nil } -func (z *zeroconf) addPrinter(name string, port uint16, ty, url, id string, online bool) error { +func (z *zeroconf) addPrinter(name string, port uint16, ty, note, url, id string, online bool) error { z.pMutex.RLock() if _, exists := z.printers[name]; exists { z.pMutex.RUnlock() @@ -52,6 +52,8 @@ func (z *zeroconf) addPrinter(name string, port uint16, ty, url, id string, onli defer C.free(unsafe.Pointer(serviceTypeC)) tyC := C.CString(ty) defer C.free(unsafe.Pointer(tyC)) + noteC := C.CString(note) + defer C.free(unsafe.Pointer(noteC)) urlC := C.CString(url) defer C.free(unsafe.Pointer(urlC)) idC := C.CString(id) @@ -65,7 +67,7 @@ func (z *zeroconf) addPrinter(name string, port uint16, ty, url, id string, onli defer C.free(unsafe.Pointer(onlineC)) var errstr *C.char = nil - service := C.startBonjour(nameC, serviceTypeC, C.ushort(port), tyC, urlC, idC, onlineC, &errstr) + service := C.startBonjour(nameC, serviceTypeC, C.ushort(port), tyC, noteC, urlC, idC, onlineC, &errstr) if errstr != nil { defer C.free(unsafe.Pointer(errstr)) return errors.New(C.GoString(errstr)) @@ -79,9 +81,11 @@ func (z *zeroconf) addPrinter(name string, port uint16, ty, url, id string, onli } // updatePrinterTXT updates the advertised TXT record. -func (z *zeroconf) updatePrinterTXT(name, ty, url, id string, online bool) error { +func (z *zeroconf) updatePrinterTXT(name, ty, note, url, id string, online bool) error { tyC := C.CString(ty) defer C.free(unsafe.Pointer(tyC)) + noteC := C.CString(note) + defer C.free(unsafe.Pointer(noteC)) urlC := C.CString(url) defer C.free(unsafe.Pointer(urlC)) idC := C.CString(id) @@ -98,7 +102,7 @@ func (z *zeroconf) updatePrinterTXT(name, ty, url, id string, online bool) error defer z.pMutex.RUnlock() if service, exists := z.printers[name]; exists { - C.updateBonjour(service, tyC, urlC, idC, onlineC) + C.updateBonjour(service, tyC, noteC, urlC, idC, onlineC) } else { return fmt.Errorf("Bonjour can't update printer %s that hasn't been added", name) } diff --git a/privet/bonjour.h b/privet/bonjour.h index ab4aaec..3e5a642 100644 --- a/privet/bonjour.h +++ b/privet/bonjour.h @@ -14,8 +14,8 @@ #include // free CFNetServiceRef startBonjour(const char *name, const char *type, - unsigned short int port, const char *ty, const char *url, const char *id, - const char *cs, char **err); -void updateBonjour(CFNetServiceRef service, const char *ty, const char *url, + unsigned short int port, const char *ty, const char *note, const char *url, + const char *id, const char *cs, char **err); +void updateBonjour(CFNetServiceRef service, const char *ty, const char *note, const char *url, const char *id, const char *cs); void stopBonjour(CFNetServiceRef service); diff --git a/privet/jobcache.go b/privet/jobcache.go index 76009b1..79b222e 100644 --- a/privet/jobcache.go +++ b/privet/jobcache.go @@ -14,8 +14,8 @@ import ( "sync" "time" - "github.com/google/cups-connector/cdd" - "github.com/google/cups-connector/log" + "github.com/google/cloud-print-connector/cdd" + "github.com/google/cloud-print-connector/log" ) // Jobs expire after this much time. diff --git a/privet/portmanager.go b/privet/portmanager.go new file mode 100644 index 0000000..f32d523 --- /dev/null +++ b/privet/portmanager.go @@ -0,0 +1,146 @@ +/* +Copyright 2016 Google Inc. All rights reserved. + +Use of this source code is governed by a BSD-style +license that can be found in the LICENSE file or at +https://developers.google.com/open-source/licenses/bsd +*/ + +package privet + +import ( + "errors" + "net" + "os" + "sync" + "syscall" + "time" +) + +var NoPortsAvailable = errors.New("No ports available") + +// portManager opens ports within the interval [low, high], starting with low. +type portManager struct { + low uint16 + high uint16 + + // Keeping a cache of used ports improves benchmark tests by over 100x. + m sync.Mutex + p map[uint16]struct{} +} + +func newPortManager(low, high uint16) *portManager { + return &portManager{ + low: low, + high: high, + p: make(map[uint16]struct{}), + } +} + +// listen finds an open port, returns an open listener on that port. +// +// Returns error when no ports are available. +func (p *portManager) listen() (*quittableListener, error) { + for port := p.nextAvailablePort(p.low); port != 0; port = p.nextAvailablePort(port) { + if l, err := newQuittableListener(port, p); err == nil { + return l, nil + } else { + if !isAddrInUse(err) { + return nil, err + } + } + } + + return nil, NoPortsAvailable +} + +// nextAvailablePort checks the p map for the next port available. +// p only keeps track of ports used by the connector, so the start parameter +// is useful to check the port after a port that is in use by some other process. +// +// Returns zero when no available port can be found. +func (p *portManager) nextAvailablePort(start uint16) uint16 { + p.m.Lock() + defer p.m.Unlock() + + for port := start; port <= p.high; port++ { + if _, exists := p.p[port]; !exists { + p.p[port] = struct{}{} + return port + } + } + + return 0 +} + +func (p *portManager) freePort(port uint16) { + p.m.Lock() + defer p.m.Unlock() + + delete(p.p, port) +} + +func isAddrInUse(err error) bool { + if err, ok := err.(*net.OpError); ok { + if err, ok := err.Err.(*os.SyscallError); ok { + return err.Err == syscall.EADDRINUSE + } + } + return false +} + +type quittableListener struct { + *net.TCPListener + + pm *portManager + + // When q is closed, the listener is quitting. + q chan struct{} +} + +func newQuittableListener(port uint16, pm *portManager) (*quittableListener, error) { + l, err := net.ListenTCP("tcp", &net.TCPAddr{Port: int(port)}) + if err != nil { + return nil, err + } + return &quittableListener{l, pm, make(chan struct{}, 0)}, nil +} + +func (l *quittableListener) Accept() (net.Conn, error) { + conn, err := l.AcceptTCP() + + select { + case <-l.q: + if err == nil { + conn.Close() + } + // The listener was closed on purpose. + // Returning an error that is not a net.Error causes net.Server.Serve() to return. + return nil, closed + default: + } + + // Clean up zombie connections. + conn.SetKeepAlive(true) + conn.SetKeepAlivePeriod(time.Minute) + + return conn, err +} + +func (l *quittableListener) Close() error { + err := l.TCPListener.Close() + if err != nil { + return err + } + l.pm.freePort(l.port()) + return nil +} + +func (l *quittableListener) port() uint16 { + return uint16(l.Addr().(*net.TCPAddr).Port) +} + +func (l *quittableListener) quit() { + close(l.q) + l.Close() +} diff --git a/privet/portmanager_test.go b/privet/portmanager_test.go new file mode 100644 index 0000000..17dd14b --- /dev/null +++ b/privet/portmanager_test.go @@ -0,0 +1,157 @@ +/* +Copyright 2016 Google Inc. All rights reserved. + +Use of this source code is governed by a BSD-style +license that can be found in the LICENSE file or at +https://developers.google.com/open-source/licenses/bsd +*/ + +package privet + +import "testing" + +const portLow = 26000 + +func TestListen_available1(t *testing.T) { + pm := newPortManager(portLow, portLow) + + l1, err := pm.listen() + if err != nil { + t.Fatal(err) + } + if l1.port() != portLow { + t.Logf("Expected port %d, got port %d", portLow, l1.port()) + l1.Close() + t.FailNow() + } + + l2, err := pm.listen() + if err == nil { + l1.Close() + l2.Close() + t.Fatal("Expected error when too many ports opened") + } + + err = l1.Close() + if err != nil { + t.Fatal(err) + } + + l3, err := pm.listen() + if err != nil { + t.Fatal(err) + } + if l3.port() != portLow { + t.Logf("Expected port %d, got port %d", portLow, l3.port()) + } + l3.Close() +} + +func TestListen_available2(t *testing.T) { + pm := newPortManager(portLow, portLow+1) + + l1, err := pm.listen() + if err != nil { + t.Fatal(err) + } + if l1.port() != portLow { + t.Logf("Expected port %d, got port %d", portLow, l1.port()) + l1.Close() + t.FailNow() + } + + l2, err := pm.listen() + if err != nil { + t.Fatal(err) + } + if l2.port() != portLow+1 { + t.Logf("Expected port %d, got port %d", portLow+1, l2.port()) + l2.Close() + t.FailNow() + } + + l3, err := pm.listen() + if err == nil { + l1.Close() + l2.Close() + l3.Close() + t.Fatal("Expected error when too many ports opened") + } + + err = l2.Close() + if err != nil { + l1.Close() + t.Fatal(err) + } + + l4, err := pm.listen() + if err != nil { + t.Fatal(err) + } + if l4.port() != portLow+1 { + t.Logf("Expected port %d, got port %d", portLow+1, l4.port()) + } + + l5, err := pm.listen() + if err == nil { + l1.Close() + l4.Close() + l5.Close() + t.Fatal("Expected error when too many ports opened") + } + + err = l1.Close() + if err != nil { + l4.Close() + t.Fatal(err) + } + + l6, err := pm.listen() + if err != nil { + t.Fatal(err) + } + if l6.port() != portLow { + t.Logf("Expected port %d, got port %d", portLow, l6.port()) + } + l4.Close() + l6.Close() +} + +// openPorts attempts to open n ports, where m are available. +func openPorts(n, m uint16) { + pm := newPortManager(portLow, portLow+m-1) + for i := uint16(0); i < n; i++ { + l, err := pm.listen() + if err == nil { + defer l.Close() + } + } +} + +func BenchmarkListen_range1_available1(*testing.B) { + openPorts(1, 1) +} + +func BenchmarkListen_range10_available10(*testing.B) { + openPorts(10, 10) +} + +func BenchmarkListen_range100_available100(*testing.B) { + openPorts(100, 100) +} + +func BenchmarkListen_range1000_available1000(*testing.B) { + openPorts(1000, 1000) +} + +func BenchmarkListen_range10_available1(*testing.B) { + openPorts(10, 1) +} + +func BenchmarkListen_range100_available10(*testing.B) { + openPorts(100, 10) +} + +func BenchmarkListen_range1000_available100(*testing.B) { + openPorts(1000, 100) +} diff --git a/privet/privet.go b/privet/privet.go index 46baee6..723ccf3 100644 --- a/privet/privet.go +++ b/privet/privet.go @@ -12,7 +12,7 @@ import ( "fmt" "sync" - "github.com/google/cups-connector/lib" + "github.com/google/cloud-print-connector/lib" ) // Privet managers local discovery and printing. @@ -21,6 +21,7 @@ type Privet struct { apis map[string]*privetAPI apisMutex sync.RWMutex // Protects apis zc *zeroconf + pm *portManager jobs chan<- *lib.Job jc jobCache @@ -32,7 +33,7 @@ type Privet struct { // NewPrivet constructs a new Privet object. // // getProximityToken should be GoogleCloudPrint.ProximityToken() -func NewPrivet(jobs chan<- *lib.Job, gcpBaseURL string, getProximityToken func(string, string) ([]byte, int, error)) (*Privet, error) { +func NewPrivet(jobs chan<- *lib.Job, portLow, portHigh uint16, gcpBaseURL string, getProximityToken func(string, string) ([]byte, int, error)) (*Privet, error) { zc, err := newZeroconf() if err != nil { return nil, err @@ -42,6 +43,7 @@ func NewPrivet(jobs chan<- *lib.Job, gcpBaseURL string, getProximityToken func(s xsrf: newXSRFSecret(), apis: make(map[string]*privetAPI), zc: zc, + pm: newPortManager(portLow, portHigh), jobs: jobs, jc: *newJobCache(), @@ -60,7 +62,12 @@ func (p *Privet) AddPrinter(printer lib.Printer, getPrinter func(string) (lib.Pr online = true } - api, err := newPrivetAPI(printer.GCPID, printer.Name, p.gcpBaseURL, p.xsrf, online, &p.jc, p.jobs, getPrinter, p.getProximityToken) + listener, err := p.pm.listen() + if err != nil { + return err + } + + api, err := newPrivetAPI(printer.GCPID, printer.Name, p.gcpBaseURL, p.xsrf, online, &p.jc, p.jobs, getPrinter, p.getProximityToken, listener) if err != nil { return err } @@ -69,7 +76,7 @@ func (p *Privet) AddPrinter(printer lib.Printer, getPrinter func(string) (lib.Pr if online { localDefaultDisplayName = fmt.Sprintf("%s (local)", localDefaultDisplayName) } - err = p.zc.addPrinter(printer.Name, api.port(), localDefaultDisplayName, p.gcpBaseURL, printer.GCPID, online) + err = p.zc.addPrinter(printer.Name, api.port(), localDefaultDisplayName, "", p.gcpBaseURL, printer.GCPID, online) if err != nil { api.quit() return err @@ -95,7 +102,7 @@ func (p *Privet) UpdatePrinter(diff *lib.PrinterDiff) error { localDefaultDisplayName = fmt.Sprintf("%s (local)", localDefaultDisplayName) } - return p.zc.updatePrinterTXT(diff.Printer.GCPID, localDefaultDisplayName, p.gcpBaseURL, diff.Printer.GCPID, online) + return p.zc.updatePrinterTXT(diff.Printer.GCPID, localDefaultDisplayName, "", p.gcpBaseURL, diff.Printer.GCPID, online) } // DeletePrinter removes a printer from Privet. diff --git a/privet/windows.go b/privet/windows.go index af5e9d7..c9f9242 100644 --- a/privet/windows.go +++ b/privet/windows.go @@ -18,11 +18,11 @@ func newZeroconf() (*zeroconf, error) { return nil, errors.New("Privet has not been implemented for Windows") } -func (z *zeroconf) addPrinter(name string, port uint16, ty, url, id string, online bool) error { +func (z *zeroconf) addPrinter(name string, port uint16, ty, note, url, id string, online bool) error { return nil } -func (z *zeroconf) updatePrinterTXT(name, ty, url, id string, online bool) error { +func (z *zeroconf) updatePrinterTXT(name, ty, note, url, id string, online bool) error { return nil } diff --git a/systemd/cloud-print-connector.service b/systemd/cloud-print-connector.service new file mode 100644 index 0000000..9cc5b90 --- /dev/null +++ b/systemd/cloud-print-connector.service @@ -0,0 +1,19 @@ +# Copyright 2016 Google Inc. All rights reserved. +# +# Use of this source code is governed by a BSD-style +# license that can be found in the LICENSE file or at +# https://developers.google.com/open-source/licenses/bsd + +[Unit] +Description=Google Cloud Print Connector +Documentation="https://github.com/google/cloud-print-connector" +After=cups.service avahi-daemon.service network-online.target +Wants=cups.service avahi-daemon.service network-online.target + +[Service] +ExecStart=/opt/cloud-print-connector/gcp-cups-connector -config-filename /opt/cloud-print-connector/gcp-cups-connector.config.json +Restart=on-failure +User=cloud-print-connector + +[Install] +WantedBy=multi-user.target diff --git a/winspool/cairo.go b/winspool/cairo.go index 030b84b..d932f35 100644 --- a/winspool/cairo.go +++ b/winspool/cairo.go @@ -1,10 +1,10 @@ -/* -Copyright 2015 Google Inc. All rights reserved. +// Copyright 2015 Google Inc. All rights reserved. -Use of this source code is governed by a BSD-style -license that can be found in the LICENSE file or at -https://developers.google.com/open-source/licenses/bsd -*/ +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// +build windows package winspool diff --git a/winspool/poppler.go b/winspool/poppler.go index 2a5cc9a..05ab64d 100644 --- a/winspool/poppler.go +++ b/winspool/poppler.go @@ -1,10 +1,10 @@ -/* -Copyright 2015 Google Inc. All rights reserved. +// Copyright 2015 Google Inc. All rights reserved. -Use of this source code is governed by a BSD-style -license that can be found in the LICENSE file or at -https://developers.google.com/open-source/licenses/bsd -*/ +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// +build windows package winspool diff --git a/winspool/utf16.go b/winspool/utf16.go index 4757ca0..41d9cc7 100644 --- a/winspool/utf16.go +++ b/winspool/utf16.go @@ -1,10 +1,10 @@ -/* -Copyright 2015 Google Inc. All rights reserved. +// Copyright 2015 Google Inc. All rights reserved. -Use of this source code is governed by a BSD-style -license that can be found in the LICENSE file or at -https://developers.google.com/open-source/licenses/bsd -*/ +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// +build windows package winspool diff --git a/winspool/win32.go b/winspool/win32.go index ab44015..47b8aca 100644 --- a/winspool/win32.go +++ b/winspool/win32.go @@ -1,10 +1,10 @@ -/* -Copyright 2015 Google Inc. All rights reserved. +// Copyright 2015 Google Inc. All rights reserved. -Use of this source code is governed by a BSD-style -license that can be found in the LICENSE file or at -https://developers.google.com/open-source/licenses/bsd -*/ +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// +build windows package winspool @@ -15,6 +15,8 @@ import ( "strings" "syscall" "unsafe" + + "golang.org/x/sys/windows" ) var ( @@ -22,26 +24,28 @@ var ( kernel32 = syscall.MustLoadDLL("kernel32.dll") ntoskrnl = syscall.MustLoadDLL("ntoskrnl.exe") winspool = syscall.MustLoadDLL("winspool.drv") - - abortDocProc = gdi32.MustFindProc("AbortDoc") - closePrinterProc = winspool.MustFindProc("ClosePrinter") - createDCProc = gdi32.MustFindProc("CreateDCW") - deleteDCProc = gdi32.MustFindProc("DeleteDC") - deviceCapabilitiesProc = winspool.MustFindProc("DeviceCapabilitiesW") - documentPropertiesProc = winspool.MustFindProc("DocumentPropertiesW") - endDocProc = gdi32.MustFindProc("EndDoc") - endPageProc = gdi32.MustFindProc("EndPage") - enumPrintersProc = winspool.MustFindProc("EnumPrintersW") - getDeviceCapsProc = gdi32.MustFindProc("GetDeviceCaps") - getJobProc = winspool.MustFindProc("GetJobW") - openPrinterProc = winspool.MustFindProc("OpenPrinterW") - resetDCProc = gdi32.MustFindProc("ResetDCW") - rtlGetVersionProc = ntoskrnl.MustFindProc("RtlGetVersion") - setGraphicsModeProc = gdi32.MustFindProc("SetGraphicsMode") - setJobProc = winspool.MustFindProc("SetJobW") - setWorldTransformProc = gdi32.MustFindProc("SetWorldTransform") - startDocProc = gdi32.MustFindProc("StartDocW") - startPageProc = gdi32.MustFindProc("StartPage") + user32 = syscall.MustLoadDLL("user32.dll") + + abortDocProc = gdi32.MustFindProc("AbortDoc") + closePrinterProc = winspool.MustFindProc("ClosePrinter") + createDCProc = gdi32.MustFindProc("CreateDCW") + deleteDCProc = gdi32.MustFindProc("DeleteDC") + deviceCapabilitiesProc = winspool.MustFindProc("DeviceCapabilitiesW") + documentPropertiesProc = winspool.MustFindProc("DocumentPropertiesW") + endDocProc = gdi32.MustFindProc("EndDoc") + endPageProc = gdi32.MustFindProc("EndPage") + enumPrintersProc = winspool.MustFindProc("EnumPrintersW") + getDeviceCapsProc = gdi32.MustFindProc("GetDeviceCaps") + getJobProc = winspool.MustFindProc("GetJobW") + openPrinterProc = winspool.MustFindProc("OpenPrinterW") + resetDCProc = gdi32.MustFindProc("ResetDCW") + rtlGetVersionProc = ntoskrnl.MustFindProc("RtlGetVersion") + setGraphicsModeProc = gdi32.MustFindProc("SetGraphicsMode") + setJobProc = winspool.MustFindProc("SetJobW") + setWorldTransformProc = gdi32.MustFindProc("SetWorldTransform") + startDocProc = gdi32.MustFindProc("StartDocW") + startPageProc = gdi32.MustFindProc("StartPage") + registerDeviceNotificationProc = user32.MustFindProc("RegisterDeviceNotificationW") ) // System error codes. @@ -99,6 +103,24 @@ const ( REG_QWORD_LITTLE_ENDIAN = 11 ) +// PRINTER_INFO_2 attribute values +const ( + PRINTER_ATTRIBUTE_QUEUED uint32 = 0x00000001 + PRINTER_ATTRIBUTE_DIRECT uint32 = 0x00000002 + PRINTER_ATTRIBUTE_DEFAULT uint32 = 0x00000004 + PRINTER_ATTRIBUTE_SHARED uint32 = 0x00000008 + PRINTER_ATTRIBUTE_NETWORK uint32 = 0x00000010 + PRINTER_ATTRIBUTE_HIDDEN uint32 = 0x00000020 + PRINTER_ATTRIBUTE_LOCAL uint32 = 0x00000040 + PRINTER_ATTRIBUTE_ENABLE_DEVQ uint32 = 0x00000080 + PRINTER_ATTRIBUTE_KEEPPRINTEDJOBS uint32 = 0x00000100 + PRINTER_ATTRIBUTE_DO_COMPLETE_FIRST uint32 = 0x00000200 + PRINTER_ATTRIBUTE_WORK_OFFLINE uint32 = 0x00000400 + PRINTER_ATTRIBUTE_ENABLE_BIDI uint32 = 0x00000800 + PRINTER_ATTRIBUTE_RAW_ONLY uint32 = 0x00001000 + PRINTER_ATTRIBUTE_PUBLISHED uint32 = 0x00002000 +) + // PRINTER_INFO_2 status values. const ( PRINTER_STATUS_PAUSED uint32 = 0x00000001 @@ -175,6 +197,10 @@ func (pi *PrinterInfo2) GetDevMode() *DevMode { return pi.pDevMode } +func (pi *PrinterInfo2) GetAttributes() uint32 { + return pi.attributes +} + func (pi *PrinterInfo2) GetStatus() uint32 { return pi.status } @@ -573,8 +599,8 @@ func enumPrinters(level uint32) ([]byte, uint32, error) { } var pPrinterEnum []byte = make([]byte, cbBuf) - _, _, err = enumPrintersProc.Call(PRINTER_ENUM_LOCAL, 0, uintptr(level), uintptr(unsafe.Pointer(&pPrinterEnum[0])), uintptr(cbBuf), uintptr(unsafe.Pointer(&cbBuf)), uintptr(unsafe.Pointer(&pcReturned))) - if err != NO_ERROR { + r1, _, err := enumPrintersProc.Call(PRINTER_ENUM_LOCAL, 0, uintptr(level), uintptr(unsafe.Pointer(&pPrinterEnum[0])), uintptr(cbBuf), uintptr(unsafe.Pointer(&cbBuf)), uintptr(unsafe.Pointer(&pcReturned))) + if r1 == 0 { return nil, 0, err } @@ -606,16 +632,16 @@ func OpenPrinter(printerName string) (HANDLE, error) { } var hPrinter HANDLE - _, _, err = openPrinterProc.Call(uintptr(unsafe.Pointer(pPrinterName)), uintptr(unsafe.Pointer(&hPrinter)), 0) - if err != NO_ERROR { + r1, _, err := openPrinterProc.Call(uintptr(unsafe.Pointer(pPrinterName)), uintptr(unsafe.Pointer(&hPrinter)), 0) + if r1 == 0 { return 0, err } return hPrinter, nil } func (hPrinter *HANDLE) ClosePrinter() error { - _, _, err := closePrinterProc.Call(uintptr(*hPrinter)) - if err != NO_ERROR { + r1, _, err := closePrinterProc.Call(uintptr(*hPrinter)) + if r1 == 0 { return err } *hPrinter = 0 @@ -726,8 +752,8 @@ func (hPrinter HANDLE) GetJob(jobID int32) (*JobInfo1, error) { } var pJob []byte = make([]byte, cbBuf) - _, _, err = getJobProc.Call(uintptr(hPrinter), uintptr(jobID), 1, uintptr(unsafe.Pointer(&pJob[0])), uintptr(cbBuf), uintptr(unsafe.Pointer(&cbBuf))) - if err != NO_ERROR { + r1, _, err := getJobProc.Call(uintptr(hPrinter), uintptr(jobID), 1, uintptr(unsafe.Pointer(&pJob[0])), uintptr(cbBuf), uintptr(unsafe.Pointer(&cbBuf))) + if r1 == 0 { return nil, err } @@ -749,12 +775,39 @@ const ( JOB_CONTROL_RELEASE uint32 = 9 ) -func (hPrinter HANDLE) SetJob(jobID int32, command uint32) error { - _, _, err := setJobProc.Call(uintptr(hPrinter), uintptr(jobID), 0, 0, uintptr(command)) - if err != NO_ERROR { +func (hPrinter HANDLE) SetJobCommand(jobID int32, command uint32) error { + r1, _, err := setJobProc.Call(uintptr(hPrinter), uintptr(jobID), 0, 0, uintptr(command)) + if r1 == 0 { + return err + } + return nil +} + +func (hPrinter HANDLE) SetJobInfo1(jobID int32, ji1 *JobInfo1) error { + r1, _, err := setJobProc.Call(uintptr(hPrinter), uintptr(jobID), 1, uintptr(unsafe.Pointer(ji1)), 0) + if r1 == 0 { + return err + } + return nil +} + +func (hPrinter HANDLE) SetJobUserName(jobID int32, userName string) error { + ji1, err := hPrinter.GetJob(jobID) + if err != nil { + return err + } + + pUserName, err := syscall.UTF16PtrFromString(userName) + if err != nil { return err } + ji1.pUserName = pUserName + ji1.position = 0 // To prevent a possible access denied error (0 is JOB_POSITION_UNSPECIFIED) + err = hPrinter.SetJobInfo1(jobID, ji1) + if err != nil { + return err + } return nil } @@ -769,7 +822,6 @@ func CreateDC(deviceName string, devMode *DevMode) (HDC, error) { if r1 == 0 { return 0, err } - return HDC(r1), nil } @@ -806,15 +858,15 @@ func (hDC HDC) StartDoc(docName string) (int32, error) { } r1, _, err := startDocProc.Call(uintptr(hDC), uintptr(unsafe.Pointer(&docInfo))) - if err != NO_ERROR { + if r1 <= 0 { return 0, err } return int32(r1), nil } func (hDC HDC) EndDoc() error { - _, _, err := endDocProc.Call(uintptr(hDC)) - if err != NO_ERROR { + r1, _, err := endDocProc.Call(uintptr(hDC)) + if r1 <= 0 { return err } return nil @@ -827,16 +879,16 @@ func (hDC HDC) AbortDoc() error { } func (hDC HDC) StartPage() error { - _, _, err := startPageProc.Call(uintptr(hDC)) - if err != NO_ERROR { + r1, _, err := startPageProc.Call(uintptr(hDC)) + if r1 <= 0 { return err } return nil } func (hDC HDC) EndPage() error { - _, _, err := endPageProc.Call(uintptr(hDC)) - if err != NO_ERROR { + r1, _, err := endPageProc.Call(uintptr(hDC)) + if r1 <= 0 { return err } return nil @@ -848,8 +900,8 @@ const ( ) func (hDC HDC) SetGraphicsMode(iMode int32) error { - _, _, err := setGraphicsModeProc.Call(uintptr(hDC), uintptr(iMode)) - if err != NO_ERROR { + r1, _, err := setGraphicsModeProc.Call(uintptr(hDC), uintptr(iMode)) + if r1 == 0 { return err } return nil @@ -1148,3 +1200,49 @@ func GetWindowsVersion() string { return version } + +type GUID struct { + Data1 uint32 + Data2 uint16 + Data3 uint16 + Data4 [8]byte +} + +var PRINTERS_DEVICE_CLASS = GUID{ + 0x4d36e979, + 0xe325, + 0x11ce, + [8]byte{0xbf, 0xc1, 0x08, 0x00, 0x2b, 0xe1, 0x03, 0x18}, +} + +type DevBroadcastDevinterface struct { + dwSize uint32 + dwDeviceType uint32 + dwReserved uint32 + classGuid GUID + szName uint16 +} + +const ( + DEVICE_NOTIFY_SERVICE_HANDLE = 1 + DEVICE_NOTIFY_ALL_INTERFACE_CLASSES = 4 + + DBT_DEVTYP_DEVICEINTERFACE = 5 +) + +func RegisterDeviceNotification(handle windows.Handle) error { + + var notificationFilter DevBroadcastDevinterface + notificationFilter.dwSize = uint32(unsafe.Sizeof(notificationFilter)) + notificationFilter.dwDeviceType = DBT_DEVTYP_DEVICEINTERFACE + notificationFilter.dwReserved = 0 + // BUG(pastarmovj): This class is ignored for now. Figure out what the right GUID is. + notificationFilter.classGuid = PRINTERS_DEVICE_CLASS + notificationFilter.szName = 0 + + r1, _, err := registerDeviceNotificationProc.Call(uintptr(handle), uintptr(unsafe.Pointer(¬ificationFilter)), DEVICE_NOTIFY_SERVICE_HANDLE|DEVICE_NOTIFY_ALL_INTERFACE_CLASSES) + if r1 == 0 { + return err + } + return nil +} diff --git a/winspool/winspool.go b/winspool/winspool.go index c8ce290..17f6369 100644 --- a/winspool/winspool.go +++ b/winspool/winspool.go @@ -1,10 +1,10 @@ -/* -Copyright 2015 Google Inc. All rights reserved. +// Copyright 2015 Google Inc. All rights reserved. -Use of this source code is governed by a BSD-style -license that can be found in the LICENSE file or at -https://developers.google.com/open-source/licenses/bsd -*/ +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +// +build windows package winspool @@ -16,8 +16,10 @@ import ( "strconv" "strings" - "github.com/google/cups-connector/cdd" - "github.com/google/cups-connector/lib" + "github.com/google/cloud-print-connector/cdd" + "github.com/google/cloud-print-connector/lib" + "github.com/google/cloud-print-connector/log" + "golang.org/x/sys/windows" ) // winspoolPDS represents capabilities that WinSpool always provides. @@ -45,10 +47,11 @@ type WinSpool struct { displayNamePrefix string systemTags map[string]string printerBlacklist map[string]interface{} + printerWhitelist map[string]interface{} } -func NewWinSpool(prefixJobIDToJobTitle bool, displayNamePrefix string, printerBlacklist []string) (*WinSpool, error) { - systemTags, err := getSystemTags() +func NewWinSpool(prefixJobIDToJobTitle bool, displayNamePrefix string, printerBlacklist []string, printerWhitelist []string, fcmNotificationsEnable bool) (*WinSpool, error) { + systemTags, err := getSystemTags(fcmNotificationsEnable) if err != nil { return nil, err } @@ -58,16 +61,22 @@ func NewWinSpool(prefixJobIDToJobTitle bool, displayNamePrefix string, printerBl pb[p] = struct{}{} } + pw := map[string]interface{}{} + for _, p := range printerWhitelist { + pw[p] = struct{}{} + } + ws := WinSpool{ prefixJobIDToJobTitle: prefixJobIDToJobTitle, displayNamePrefix: displayNamePrefix, systemTags: systemTags, printerBlacklist: pb, + printerWhitelist: pw, } return &ws, nil } -func getSystemTags() (map[string]string, error) { +func getSystemTags(fcmNotificationsEnable bool) (map[string]string, error) { tags := make(map[string]string) tags["connector-version"] = lib.BuildDate @@ -77,12 +86,17 @@ func getSystemTags() (map[string]string, error) { } tags["system-arch"] = runtime.GOARCH tags["system-golang-version"] = runtime.Version() + if fcmNotificationsEnable { + tags["system-notifications-channel"] = "fcm" + } else { + tags["system-notifications-channel"] = "xmpp" + } tags["system-windows-version"] = GetWindowsVersion() return tags, nil } -func convertPrinterState(wsStatus uint32) *cdd.PrinterStateSection { +func convertPrinterState(wsStatus uint32, wsAttributes uint32) *cdd.PrinterStateSection { state := cdd.PrinterStateSection{ State: cdd.CloudDeviceStateIdle, VendorState: &cdd.VendorState{}, @@ -147,7 +161,12 @@ func convertPrinterState(wsStatus uint32) *cdd.PrinterStateSection { } state.VendorState.Item = append(state.VendorState.Item, vs) } - if wsStatus&PRINTER_STATUS_OFFLINE != 0 { + + // If PRINTER_ATTRIBUTE_WORK_OFFLINE is set + // spooler won't despool any jobs to the printer. + // At least for some USB printers, this flag is controlled + // automatically by the system depending on the state of physical connection. + if wsStatus&PRINTER_STATUS_OFFLINE != 0 || wsAttributes&PRINTER_ATTRIBUTE_WORK_OFFLINE != 0 { state.State = cdd.CloudDeviceStateStopped vs := cdd.VendorStateItem{ State: cdd.VendorStateError, @@ -300,18 +319,30 @@ func (ws *WinSpool) GetPrinters() ([]lib.Printer, error) { printers := make([]lib.Printer, 0, len(pi2s)) for _, pi2 := range pi2s { printerName := pi2.GetPrinterName() + + // Check whitelist/blacklist in loop once we have printerName. + // Avoids unnecessary processing of excluded printers. + if _, exists := ws.printerBlacklist[printerName]; exists { + log.Debugf("Ignoring blacklisted printer %s", printerName) + continue + } + if len(ws.printerWhitelist) != 0 { + if _, exists := ws.printerWhitelist[printerName]; !exists { + log.Debugf("Ignoring non-whitelisted printer %s", printerName) + continue + } + } portName := pi2.GetPortName() devMode := pi2.GetDevMode() manufacturer, model := getManModel(pi2.GetDriverName()) - printer := lib.Printer{ Name: printerName, DefaultDisplayName: ws.displayNamePrefix + printerName, UUID: printerName, // TODO: Add something unique from host. Manufacturer: manufacturer, Model: model, - State: convertPrinterState(pi2.GetStatus()), + State: convertPrinterState(pi2.GetStatus(), pi2.GetAttributes()), Description: &cdd.PrinterDescriptionSection{}, Tags: map[string]string{ "printer-location": pi2.GetLocation(), @@ -431,23 +462,12 @@ func (ws *WinSpool) GetPrinters() ([]lib.Printer, error) { printers = append(printers, printer) } - printers = ws.filterBlacklistPrinters(printers) printers = addStaticDescriptionToPrinters(printers) printers = ws.addSystemTagsToPrinters(printers) return printers, nil } -func (ws *WinSpool) filterBlacklistPrinters(printers []lib.Printer) []lib.Printer { - result := make([]lib.Printer, 0, len(printers)) - for i := range printers { - if _, exists := ws.printerBlacklist[printers[i].Name]; !exists { - result = append(result, printers[i]) - } - } - return result -} - // addStaticDescriptionToPrinters adds information that is true for all // printers to printers. func addStaticDescriptionToPrinters(printers []lib.Printer) []lib.Printer { @@ -545,8 +565,7 @@ func convertJobState(wsStatus uint32) *cdd.JobState { state.Type = cdd.JobStateDone } else if wsStatus&JOB_STATUS_PAUSED != 0 || wsStatus == 0 { - state.Type = cdd.JobStateStopped - state.UserActionCause = &cdd.UserActionCause{cdd.UserActionCausePaused} + state.Type = cdd.JobStateDone } else if wsStatus&JOB_STATUS_ERROR != 0 { state.Type = cdd.JobStateAborted @@ -606,7 +625,7 @@ type jobContext struct { cContext CairoContext } -func newJobContext(printerName, fileName, title string) (*jobContext, error) { +func newJobContext(printerName, fileName, title, user string) (*jobContext, error) { pDoc, err := PopplerDocumentNewFromFile(fileName) if err != nil { return nil, err @@ -641,14 +660,7 @@ func newJobContext(printerName, fileName, title string) (*jobContext, error) { pDoc.Unref() return nil, err } - err = hPrinter.SetJob(jobID, JOB_CONTROL_RETAIN) - if err != nil { - hDC.EndDoc() - hDC.DeleteDC() - hPrinter.ClosePrinter() - pDoc.Unref() - return nil, err - } + hPrinter.SetJobUserName(jobID, user) cSurface, err := CairoWin32PrintingSurfaceCreate(hDC) if err != nil { hDC.EndDoc() @@ -680,10 +692,6 @@ func (c *jobContext) free() error { if err != nil { return err } - err = c.hPrinter.SetJob(c.jobID, JOB_CONTROL_RELEASE) - if err != nil { - return err - } err = c.hDC.EndDoc() if err != nil { return err @@ -829,7 +837,7 @@ func (ws *WinSpool) Print(printer *lib.Printer, fileName, title, user, gcpJobID return 0, errors.New("Print() called with nil ticket") } - jobContext, err := newJobContext(printer.Name, fileName, title) + jobContext, err := newJobContext(printer.Name, fileName, title, user) if err != nil { return 0, err } @@ -902,10 +910,48 @@ func (ws *WinSpool) Print(printer *lib.Printer, fileName, title, user, gcpJobID } } + // Retain unpaused jobs to check the status later. Don't retain paused jobs because + // release would delete the job even if it was still paused and hadn't been printed + ji1, err := jobContext.hPrinter.GetJob(jobContext.jobID) + if err != nil { + return 0, err + } + if ji1.status&JOB_STATUS_PAUSED == 0 { + err = jobContext.hPrinter.SetJobCommand(jobContext.jobID, JOB_CONTROL_RETAIN) + if err != nil { + return 0, err + } + } + return uint32(jobContext.jobID), nil } +func (ws *WinSpool) ReleaseJob(printerName string, jobID uint32) error { + hPrinter, err := OpenPrinter(printerName) + if err != nil { + return err + } + + // Only release if the job was retained (otherwise we get an error) + ji1, err := hPrinter.GetJob(int32(jobID)) + if err != nil { + return err + } + if ji1.status&JOB_STATUS_RETAINED != 0 { + err = hPrinter.SetJobCommand(int32(jobID), JOB_CONTROL_RELEASE) + if err != nil { + return err + } + } + + return nil +} + +func (ws *WinSpool) StartPrinterNotifications(handle windows.Handle) error { + err := RegisterDeviceNotification(handle) + return err +} + // The following functions are not relevant to Windows printing, but are required by the NativePrintSystem interface. -func (ws *WinSpool) Quit() {} func (ws *WinSpool) RemoveCachedPPD(printerName string) {} diff --git a/wix/LICENSE.rtf b/wix/LICENSE.rtf new file mode 100644 index 0000000..c121875 Binary files /dev/null and b/wix/LICENSE.rtf differ diff --git a/wix/README.md b/wix/README.md new file mode 100644 index 0000000..f024bac --- /dev/null +++ b/wix/README.md @@ -0,0 +1,65 @@ +# Windows Installer + +## Build Requirements +The WIX toolset is required to build the Windows Installer file. +It can be downloaded from http://wixtoolset.org. + +## Build Instructions +Build the Cloud Print Connector binaries. See https://github.com/google/cloud-print-connector/wiki/Build-from-source + +Update the dependencies.wxs file by running ./generate-dependencies.sh (in mingw64 bash shell). + +Use the WIX tools to build the MSI. The WIX tools that are used are candle.exe +and light.exe. They are installed by default to +"C:\Program Files (x86)\WiX Toolset v3.10\bin" +(/c/Program\ Files\ (x86)/WiX\ Toolset\ v3.10/bin/light.exe if you're using +mingw bash shell). You can add this directory to your PATH to run the following +two commands. + +Run candle.exe to build wixobj file from the wxs file: +``` +candle.exe -arch x64 windows-connector.wxs dependencies.wxs +``` + +Expected output: +> Windows Installer XML Toolset Compiler version 3.10.2.2516 +> Copyright (c) Outercurve Foundation. All rights reserved. +> +> windows-connector.wxs +> dependencies.wxs + + +Run light.exe to build MSI file from the wixobj +``` +light.exe -ext "C:\Program Files (x86)\WiX Toolset v3.10\bin\WixUIExtension.dll" windows-connector.wixobj dependencies.wixobj -o windows-connector.msi +``` + +Expected output: +> Windows Installer XML Toolset Linker version 3.10.2.2516 +> Copyright (c) Outercurve Foundation. All rights reserved. + +The light.exe command line requires the path of WixUIExtension.dll which +provides the UI that is used by this installer. If the WIX toolset is installed +to a different directory, use that directory path for the UI extension dll. + +If the built Windows Connector binaries are not in $GOPATH\bin, then add -dSourceDir= +to the light.exe command line to specify where the files can be found. + +If mingw64 is not installed to C:\msys64\mingw64, then use -dDependencyDir= +to specify where it is installed. + +## Installation Instructions +Install the MSI by any normal method of installing an MSI file (double-clicking, automated deployment, etc.) + +During an installation with UI, gcp-connector-util init will be run as the last step which +will open a console window to initialize the connector. + +The following public properties may be set during install of the MSI +(see https://msdn.microsoft.com/en-us/library/windows/desktop/aa370912(v=vs.85).aspx) +* CONFIGFILE = Path of connector config file to use instead of running gcp-connector-util init during install + +## Modifying the Config File after install +The installer will create (or copy) the config file specified to the Common +Application Data directory at %PROGRAMDATA%\Google\Cloud Print Connector. +This is the file that is used by the connector. This file can be modified +and the service restarted to change the configuration. diff --git a/wix/build-msi.sh b/wix/build-msi.sh new file mode 100644 index 0000000..7085157 --- /dev/null +++ b/wix/build-msi.sh @@ -0,0 +1,56 @@ +if [ $# -eq 0 ]; then + me=$(basename $0) + echo "Usage: $me " + exit 1 +fi +export CONNECTOR_VERSION=$1 +LDFLAGS="github.com/google/cloud-print-connector/lib.BuildDate=$CONNECTOR_VERSION" +CONNECTOR_DIR=$GOPATH/src/github.com/google/cloud-print-connector + +arch=$(arch) +if [[ "$arch" == "i686" ]]; then + wixarch="x86" +elif [[ "$arch" == "x86_64" ]]; then + wixarch="x64" +fi + +MSI_FILE="$CONNECTOR_DIR/wix/windows-connector-$CONNECTOR_VERSION-$arch.msi" + +echo "Running go get..." +go get -ldflags -X="$LDFLAGS" -v github.com/google/cloud-print-connector/... +rc=$? +if [[ $rc != 0 ]]; then + echo "Error $rc with go get. Exiting." + exit $rc +fi + +echo "Running generate-dependencies.sh..." +$CONNECTOR_DIR/wix/generate-dependencies.sh +rc=$? +if [[ $rc != 0 ]]; then + echo "Error $rc with generate-dependencies.sh. Exiting." + exit $rc +fi + +echo "Running WIX candle.exe..." +"$WIX/bin/candle.exe" -arch $wixarch "$CONNECTOR_DIR/wix/windows-connector-$wixarch.wxs" \ + "$CONNECTOR_DIR/wix/dependencies.wxs" +rc=$? +if [[ $rc != 0 ]]; then + echo "Error $rc with WIX candle.exe. Exiting." + exit $rc +fi + +echo "Running WIX light.exe..." +"$WIX/bin/light.exe" -ext "$WIX/bin/WixUIExtension.dll" \ + "$CONNECTOR_DIR/wix/windows-connector.wixobj" "$CONNECTOR_DIR/wix/dependencies.wixobj" \ + -o "$MSI_FILE" +rc=$? +if [[ $rc != 0 ]]; then + echo "Error $rc with WIX light.exe. Exiting." + exit $rc +fi + +rm $CONNECTOR_DIR/wix/dependencies.wxs + +echo "Successfully generated $MSI_FILE" diff --git a/wix/generate-dependencies.sh b/wix/generate-dependencies.sh new file mode 100644 index 0000000..a22bdd4 --- /dev/null +++ b/wix/generate-dependencies.sh @@ -0,0 +1,16 @@ +#!/usr/bin/bash +echo ''>dependencies.wxs +echo ' + + '>>dependencies.wxs +for f in `ldd ${GOPATH}/bin/gcp-windows-connector.exe | grep -i -v Windows | sed s/" =>.*"// | sed s/"\t"// | sort` + do echo " + + ">>dependencies.wxs; done +echo ' + +'>>dependencies.wxs + diff --git a/wix/windows-connector-x64.wxs b/wix/windows-connector-x64.wxs new file mode 100644 index 0000000..ebe4b6c --- /dev/null +++ b/wix/windows-connector-x64.wxs @@ -0,0 +1,174 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + STARTSERVICE="YES" + + + + + + + + + + + + + + + NOT CONFIGFILE + + + CONFIGFILE + + + + + + + + + + NOT CONFIGFILE and NOT Installed and NOT WIX_UPGRADE_DETECTED and RUNINIT="YES" + + + + + + + DELETEPRINTERS="YES" AND $ConfigFile=2 + + + + + + diff --git a/wix/windows-connector-x86.wxs b/wix/windows-connector-x86.wxs new file mode 100644 index 0000000..76d6dc6 --- /dev/null +++ b/wix/windows-connector-x86.wxs @@ -0,0 +1,174 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + STARTSERVICE="YES" + + + + + + + + + + + + + + + NOT CONFIGFILE + + + CONFIGFILE + + + + + + + + + + NOT CONFIGFILE and NOT Installed and NOT WIX_UPGRADE_DETECTED and RUNINIT="YES" + + + + + + + DELETEPRINTERS="YES" AND $ConfigFile=2 + + + + + + diff --git a/xmpp/internal-xmpp.go b/xmpp/internal-xmpp.go index 0e7ac75..44b8523 100644 --- a/xmpp/internal-xmpp.go +++ b/xmpp/internal-xmpp.go @@ -24,7 +24,8 @@ import ( "strings" "time" - "github.com/google/cups-connector/log" + "github.com/google/cloud-print-connector/log" + "github.com/google/cloud-print-connector/notification" ) const ( @@ -42,7 +43,7 @@ type internalXMPP struct { xmlDecoder *xml.Decoder fullJID string - notifications chan<- PrinterNotification + notifications chan<- notification.PrinterNotification pongs chan uint8 nextPingID uint8 dead chan<- struct{} @@ -53,7 +54,7 @@ type internalXMPP struct { // Received XMPP notifications are sent on the notifications channel. // // If the connection dies unexpectedly, a message is sent on dead. -func newInternalXMPP(jid, accessToken, proxyName, server string, port uint16, pingTimeout, pingInterval time.Duration, notifications chan<- PrinterNotification, dead chan<- struct{}) (*internalXMPP, error) { +func newInternalXMPP(jid, accessToken, proxyName, server string, port uint16, pingTimeout, pingInterval time.Duration, notifications chan<- notification.PrinterNotification, dead chan<- struct{}) (*internalXMPP, error) { var user, domain string if parts := strings.SplitN(jid, "@", 2); len(parts) != 2 { return nil, fmt.Errorf("Tried to use invalid XMPP JID: %s", jid) @@ -188,11 +189,11 @@ func (x *internalXMPP) dispatchIncoming(dying chan<- struct{}) { if strings.ContainsRune(messageDataString, '/') { if strings.HasSuffix(messageDataString, "/delete") { gcpID := strings.TrimSuffix(messageDataString, "/delete") - x.notifications <- PrinterNotification{gcpID, PrinterDelete} + x.notifications <- notification.PrinterNotification{gcpID, notification.PrinterDelete} } // Ignore other suffixes, like /update_settings. } else { - x.notifications <- PrinterNotification{messageDataString, PrinterNewJobs} + x.notifications <- notification.PrinterNotification{messageDataString, notification.PrinterNewJobs} } } else if startElement.Name.Local == "iq" { diff --git a/xmpp/xmpp.go b/xmpp/xmpp.go index 84f1f6e..5326108 100644 --- a/xmpp/xmpp.go +++ b/xmpp/xmpp.go @@ -12,21 +12,12 @@ import ( "fmt" "time" - "github.com/google/cups-connector/log" + "github.com/google/cloud-print-connector/log" + "github.com/google/cloud-print-connector/notification" ) type PrinterNotificationType uint8 -const ( - PrinterNewJobs PrinterNotificationType = iota - PrinterDelete -) - -type PrinterNotification struct { - GCPID string - Type PrinterNotificationType -} - type XMPP struct { jid string proxyName string @@ -36,7 +27,7 @@ type XMPP struct { pingInterval time.Duration getAccessToken func() (string, error) - notifications chan<- PrinterNotification + notifications chan<- notification.PrinterNotification dead chan struct{} quit chan struct{} @@ -44,7 +35,7 @@ type XMPP struct { ix *internalXMPP } -func NewXMPP(jid, proxyName, server string, port uint16, pingTimeout, pingInterval time.Duration, getAccessToken func() (string, error), notifications chan<- PrinterNotification) (*XMPP, error) { +func NewXMPP(jid, proxyName, server string, port uint16, pingTimeout, pingInterval time.Duration, getAccessToken func() (string, error), notifications chan<- notification.PrinterNotification) (*XMPP, error) { x := XMPP{ jid: jid, proxyName: proxyName, @@ -58,9 +49,12 @@ func NewXMPP(jid, proxyName, server string, port uint16, pingTimeout, pingInterv quit: make(chan struct{}), } - err := x.startXMPP() - if err != nil { - return nil, err + if err := x.startXMPP(); err != nil { + for err != nil { + log.Errorf("XMPP start failed, will try again in 10s: %s", err) + time.Sleep(10 * time.Second) + err = x.startXMPP() + } } go x.keepXMPPAlive() @@ -81,7 +75,6 @@ func (x *XMPP) Quit() { } // startXMPP tries to start an XMPP conversation. -// Tries multiple times before returning an error. func (x *XMPP) startXMPP() error { if x.ix != nil { go x.ix.Quit() diff --git a/xmpp/xmpp_test.go b/xmpp/xmpp_test.go index 0396696..b94bf8c 100644 --- a/xmpp/xmpp_test.go +++ b/xmpp/xmpp_test.go @@ -24,7 +24,8 @@ import ( "testing" "time" - "github.com/google/cups-connector/xmpp" + "github.com/google/cloud-print-connector/notification" + "github.com/google/cloud-print-connector/xmpp" ) func TestXMPP_proxyauth(t *testing.T) { @@ -53,7 +54,7 @@ func TestXMPP_proxyauth(t *testing.T) { t.Fatal(err) } - ch := make(chan<- xmpp.PrinterNotification) + ch := make(chan<- notification.PrinterNotification) x, err := xmpp.NewXMPP("jid@example.com", "proxyName", strs[0], uint16(port), time.Minute, time.Minute, func() (string, error) { return "accessToken", nil }, ch) @@ -85,7 +86,7 @@ func testXMPP_reconnect(t *testing.T) { http.DefaultTransport = orig }() - ch := make(chan<- xmpp.PrinterNotification) + ch := make(chan<- notification.PrinterNotification) x, err := xmpp.NewXMPP("jid@example.com", "proxyName", "127.0.0.1", ts.port, time.Minute, time.Minute, func() (string, error) { return "accessToken", nil }, ch) @@ -113,7 +114,7 @@ func TestXMPP_ping(t *testing.T) { http.DefaultTransport = orig }() - ch := make(chan<- xmpp.PrinterNotification) + ch := make(chan<- notification.PrinterNotification) x, err := xmpp.NewXMPP("jid@example.com", "proxyName", "127.0.0.1", ts.port, time.Second, time.Second, func() (string, error) { return "accessToken", nil }, ch) @@ -145,7 +146,7 @@ func testXMPP_pingtimeout(t *testing.T) { http.DefaultTransport = orig }() - ch := make(chan<- xmpp.PrinterNotification) + ch := make(chan<- notification.PrinterNotification) x, err := xmpp.NewXMPP("jid@example.com", "proxyName", "127.0.0.1", ts.port, time.Millisecond, time.Millisecond, func() (string, error) { return "accessToken", nil }, ch)