1
0
Fork 0

Vendor integration dependencies.

This commit is contained in:
Timo Reimann 2017-02-07 22:33:23 +01:00
parent dd5e3fba01
commit 55b57c736b
2451 changed files with 731611 additions and 0 deletions

191
integration/vendor/github.com/docker/libcompose/LICENSE generated vendored Normal file
View file

@ -0,0 +1,191 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright 2015 Docker, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View file

@ -0,0 +1,44 @@
package config
import (
"github.com/docker/libcompose/utils"
"github.com/docker/libcompose/yaml"
)
// ConvertServices converts a set of v1 service configs to v2 service configs
func ConvertServices(v1Services map[string]*ServiceConfigV1) (map[string]*ServiceConfig, error) {
v2Services := make(map[string]*ServiceConfig)
replacementFields := make(map[string]*ServiceConfig)
for name, service := range v1Services {
replacementFields[name] = &ServiceConfig{
Build: yaml.Build{
Context: service.Build,
Dockerfile: service.Dockerfile,
},
Logging: Log{
Driver: service.LogDriver,
Options: service.LogOpt,
},
NetworkMode: service.Net,
}
v1Services[name].Build = ""
v1Services[name].Dockerfile = ""
v1Services[name].LogDriver = ""
v1Services[name].LogOpt = nil
v1Services[name].Net = ""
}
if err := utils.Convert(v1Services, &v2Services); err != nil {
return nil, err
}
for name := range v2Services {
v2Services[name].Build = replacementFields[name].Build
v2Services[name].Logging = replacementFields[name].Logging
v2Services[name].NetworkMode = replacementFields[name].NetworkMode
}
return v2Services, nil
}

View file

@ -0,0 +1,95 @@
package config
import (
"crypto/sha1"
"encoding/hex"
"fmt"
"io"
"reflect"
"sort"
"github.com/docker/libcompose/yaml"
)
// GetServiceHash computes and returns a hash that will identify a service.
// This hash will be then used to detect if the service definition/configuration
// have changed and needs to be recreated.
func GetServiceHash(name string, config *ServiceConfig) string {
hash := sha1.New()
io.WriteString(hash, name)
//Get values of Service through reflection
val := reflect.ValueOf(config).Elem()
//Create slice to sort the keys in Service Config, which allow constant hash ordering
serviceKeys := []string{}
//Create a data structure of map of values keyed by a string
unsortedKeyValue := make(map[string]interface{})
//Get all keys and values in Service Configuration
for i := 0; i < val.NumField(); i++ {
valueField := val.Field(i)
keyField := val.Type().Field(i)
serviceKeys = append(serviceKeys, keyField.Name)
unsortedKeyValue[keyField.Name] = valueField.Interface()
}
//Sort serviceKeys alphabetically
sort.Strings(serviceKeys)
//Go through keys and write hash
for _, serviceKey := range serviceKeys {
serviceValue := unsortedKeyValue[serviceKey]
io.WriteString(hash, fmt.Sprintf("\n %v: ", serviceKey))
switch s := serviceValue.(type) {
case yaml.SliceorMap:
sliceKeys := []string{}
for lkey := range s {
sliceKeys = append(sliceKeys, lkey)
}
sort.Strings(sliceKeys)
for _, sliceKey := range sliceKeys {
io.WriteString(hash, fmt.Sprintf("%s=%v, ", sliceKey, s[sliceKey]))
}
case yaml.MaporEqualSlice:
for _, sliceKey := range s {
io.WriteString(hash, fmt.Sprintf("%s, ", sliceKey))
}
case yaml.MaporColonSlice:
for _, sliceKey := range s {
io.WriteString(hash, fmt.Sprintf("%s, ", sliceKey))
}
case yaml.MaporSpaceSlice:
for _, sliceKey := range s {
io.WriteString(hash, fmt.Sprintf("%s, ", sliceKey))
}
case yaml.Command:
for _, sliceKey := range s {
io.WriteString(hash, fmt.Sprintf("%s, ", sliceKey))
}
case yaml.Stringorslice:
sort.Strings(s)
for _, sliceKey := range s {
io.WriteString(hash, fmt.Sprintf("%s, ", sliceKey))
}
case []string:
sliceKeys := s
sort.Strings(sliceKeys)
for _, sliceKey := range sliceKeys {
io.WriteString(hash, fmt.Sprintf("%s, ", sliceKey))
}
default:
io.WriteString(hash, fmt.Sprintf("%v", serviceValue))
}
}
return hex.EncodeToString(hash.Sum(nil))
}

View file

@ -0,0 +1,169 @@
package config
import (
"bytes"
"fmt"
"strings"
"github.com/Sirupsen/logrus"
)
func isNum(c uint8) bool {
return c >= '0' && c <= '9'
}
func validVariableNameChar(c uint8) bool {
return c == '_' ||
c >= 'A' && c <= 'Z' ||
c >= 'a' && c <= 'z' ||
isNum(c)
}
func parseVariable(line string, pos int, mapping func(string) string) (string, int, bool) {
var buffer bytes.Buffer
for ; pos < len(line); pos++ {
c := line[pos]
switch {
case validVariableNameChar(c):
buffer.WriteByte(c)
default:
return mapping(buffer.String()), pos - 1, true
}
}
return mapping(buffer.String()), pos, true
}
func parseVariableWithBraces(line string, pos int, mapping func(string) string) (string, int, bool) {
var buffer bytes.Buffer
for ; pos < len(line); pos++ {
c := line[pos]
switch {
case c == '}':
bufferString := buffer.String()
if bufferString == "" {
return "", 0, false
}
return mapping(buffer.String()), pos, true
case validVariableNameChar(c):
buffer.WriteByte(c)
default:
return "", 0, false
}
}
return "", 0, false
}
func parseInterpolationExpression(line string, pos int, mapping func(string) string) (string, int, bool) {
c := line[pos]
switch {
case c == '$':
return "$", pos, true
case c == '{':
return parseVariableWithBraces(line, pos+1, mapping)
case !isNum(c) && validVariableNameChar(c):
// Variables can't start with a number
return parseVariable(line, pos, mapping)
default:
return "", 0, false
}
}
func parseLine(line string, mapping func(string) string) (string, bool) {
var buffer bytes.Buffer
for pos := 0; pos < len(line); pos++ {
c := line[pos]
switch {
case c == '$':
var replaced string
var success bool
replaced, pos, success = parseInterpolationExpression(line, pos+1, mapping)
if !success {
return "", false
}
buffer.WriteString(replaced)
default:
buffer.WriteByte(c)
}
}
return buffer.String(), true
}
func parseConfig(option, service string, data *interface{}, mapping func(string) string) error {
switch typedData := (*data).(type) {
case string:
var success bool
*data, success = parseLine(typedData, mapping)
if !success {
return fmt.Errorf("Invalid interpolation format for \"%s\" option in service \"%s\": \"%s\"", option, service, typedData)
}
case []interface{}:
for k, v := range typedData {
err := parseConfig(option, service, &v, mapping)
if err != nil {
return err
}
typedData[k] = v
}
case map[interface{}]interface{}:
for k, v := range typedData {
err := parseConfig(option, service, &v, mapping)
if err != nil {
return err
}
typedData[k] = v
}
}
return nil
}
// Interpolate replaces variables in the raw map representation of the project file
func Interpolate(environmentLookup EnvironmentLookup, config *RawServiceMap) error {
for k, v := range *config {
for k2, v2 := range v {
err := parseConfig(k2, k, &v2, func(s string) string {
values := environmentLookup.Lookup(s, k, nil)
if len(values) == 0 {
logrus.Warnf("The %s variable is not set. Substituting a blank string.", s)
return ""
}
// Use first result if many are given
value := values[0]
// Environment variables come in key=value format
// Return everything past first '='
return strings.SplitN(value, "=", 2)[1]
})
if err != nil {
return err
}
(*config)[k][k2] = v2
}
}
return nil
}

View file

@ -0,0 +1,166 @@
package config
import (
"bufio"
"bytes"
"fmt"
"strings"
yaml "github.com/cloudfoundry-incubator/candiedyaml"
"github.com/docker/docker/pkg/urlutil"
)
var (
noMerge = []string{
"links",
"volumes_from",
}
defaultParseOptions = ParseOptions{
Interpolate: true,
Validate: true,
}
)
// Merge merges a compose file into an existing set of service configs
func Merge(existingServices *ServiceConfigs, environmentLookup EnvironmentLookup, resourceLookup ResourceLookup, file string, bytes []byte, options *ParseOptions) (string, map[string]*ServiceConfig, map[string]*VolumeConfig, map[string]*NetworkConfig, error) {
if options == nil {
options = &defaultParseOptions
}
var config Config
if err := yaml.Unmarshal(bytes, &config); err != nil {
return "", nil, nil, nil, err
}
var serviceConfigs map[string]*ServiceConfig
var volumeConfigs map[string]*VolumeConfig
var networkConfigs map[string]*NetworkConfig
if config.Version == "2" {
var err error
serviceConfigs, err = MergeServicesV2(existingServices, environmentLookup, resourceLookup, file, bytes, options)
if err != nil {
return "", nil, nil, nil, err
}
volumeConfigs, err = ParseVolumes(bytes)
if err != nil {
return "", nil, nil, nil, err
}
networkConfigs, err = ParseNetworks(bytes)
if err != nil {
return "", nil, nil, nil, err
}
} else {
serviceConfigsV1, err := MergeServicesV1(existingServices, environmentLookup, resourceLookup, file, bytes, options)
if err != nil {
return "", nil, nil, nil, err
}
serviceConfigs, err = ConvertServices(serviceConfigsV1)
if err != nil {
return "", nil, nil, nil, err
}
}
adjustValues(serviceConfigs)
if options.Postprocess != nil {
var err error
serviceConfigs, err = options.Postprocess(serviceConfigs)
if err != nil {
return "", nil, nil, nil, err
}
}
return config.Version, serviceConfigs, volumeConfigs, networkConfigs, nil
}
func adjustValues(configs map[string]*ServiceConfig) {
// yaml parser turns "no" into "false" but that is not valid for a restart policy
for _, v := range configs {
if v.Restart == "false" {
v.Restart = "no"
}
}
}
func readEnvFile(resourceLookup ResourceLookup, inFile string, serviceData RawService) (RawService, error) {
if _, ok := serviceData["env_file"]; !ok {
return serviceData, nil
}
envFiles := serviceData["env_file"].([]interface{})
if len(envFiles) == 0 {
return serviceData, nil
}
if resourceLookup == nil {
return nil, fmt.Errorf("Can not use env_file in file %s no mechanism provided to load files", inFile)
}
var vars []interface{}
if _, ok := serviceData["environment"]; ok {
vars = serviceData["environment"].([]interface{})
}
for i := len(envFiles) - 1; i >= 0; i-- {
envFile := envFiles[i].(string)
content, _, err := resourceLookup.Lookup(envFile, inFile)
if err != nil {
return nil, err
}
if err != nil {
return nil, err
}
scanner := bufio.NewScanner(bytes.NewBuffer(content))
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
key := strings.SplitAfter(line, "=")[0]
found := false
for _, v := range vars {
if strings.HasPrefix(v.(string), key) {
found = true
break
}
}
if !found {
vars = append(vars, line)
}
}
if scanner.Err() != nil {
return nil, scanner.Err()
}
}
serviceData["environment"] = vars
delete(serviceData, "env_file")
return serviceData, nil
}
func mergeConfig(baseService, serviceData RawService) RawService {
for k, v := range serviceData {
// Image and build are mutually exclusive in merge
if k == "image" {
delete(baseService, "build")
} else if k == "build" {
delete(baseService, "image")
}
existing, ok := baseService[k]
if ok {
baseService[k] = merge(existing, v)
} else {
baseService[k] = v
}
}
return baseService
}
// IsValidRemote checks if the specified string is a valid remote (for builds)
func IsValidRemote(remote string) bool {
return urlutil.IsGitURL(remote) || urlutil.IsURL(remote)
}

View file

@ -0,0 +1,199 @@
package config
import (
"fmt"
"path"
"github.com/Sirupsen/logrus"
yaml "github.com/cloudfoundry-incubator/candiedyaml"
"github.com/docker/libcompose/utils"
)
// MergeServicesV1 merges a v1 compose file into an existing set of service configs
func MergeServicesV1(existingServices *ServiceConfigs, environmentLookup EnvironmentLookup, resourceLookup ResourceLookup, file string, bytes []byte, options *ParseOptions) (map[string]*ServiceConfigV1, error) {
datas := make(RawServiceMap)
if err := yaml.Unmarshal(bytes, &datas); err != nil {
return nil, err
}
if options.Interpolate {
if err := Interpolate(environmentLookup, &datas); err != nil {
return nil, err
}
}
if options.Preprocess != nil {
var err error
datas, err = options.Preprocess(datas)
if err != nil {
return nil, err
}
}
if options.Validate {
if err := validate(datas); err != nil {
return nil, err
}
}
for name, data := range datas {
data, err := parseV1(resourceLookup, environmentLookup, file, data, datas, options)
if err != nil {
logrus.Errorf("Failed to parse service %s: %v", name, err)
return nil, err
}
if serviceConfig, ok := existingServices.Get(name); ok {
var rawExistingService RawService
if err := utils.Convert(serviceConfig, &rawExistingService); err != nil {
return nil, err
}
data = mergeConfig(rawExistingService, data)
}
datas[name] = data
}
if options.Validate {
for name, data := range datas {
err := validateServiceConstraints(data, name)
if err != nil {
return nil, err
}
}
}
serviceConfigs := make(map[string]*ServiceConfigV1)
if err := utils.Convert(datas, &serviceConfigs); err != nil {
return nil, err
}
return serviceConfigs, nil
}
func parseV1(resourceLookup ResourceLookup, environmentLookup EnvironmentLookup, inFile string, serviceData RawService, datas RawServiceMap, options *ParseOptions) (RawService, error) {
serviceData, err := readEnvFile(resourceLookup, inFile, serviceData)
if err != nil {
return nil, err
}
serviceData = resolveContextV1(inFile, serviceData)
value, ok := serviceData["extends"]
if !ok {
return serviceData, nil
}
mapValue, ok := value.(map[interface{}]interface{})
if !ok {
return serviceData, nil
}
if resourceLookup == nil {
return nil, fmt.Errorf("Can not use extends in file %s no mechanism provided to files", inFile)
}
file := asString(mapValue["file"])
service := asString(mapValue["service"])
if service == "" {
return serviceData, nil
}
var baseService RawService
if file == "" {
if serviceData, ok := datas[service]; ok {
baseService, err = parseV1(resourceLookup, environmentLookup, inFile, serviceData, datas, options)
} else {
return nil, fmt.Errorf("Failed to find service %s to extend", service)
}
} else {
bytes, resolved, err := resourceLookup.Lookup(file, inFile)
if err != nil {
logrus.Errorf("Failed to lookup file %s: %v", file, err)
return nil, err
}
var baseRawServices RawServiceMap
if err := yaml.Unmarshal(bytes, &baseRawServices); err != nil {
return nil, err
}
if options.Interpolate {
err = Interpolate(environmentLookup, &baseRawServices)
if err != nil {
return nil, err
}
}
if options.Preprocess != nil {
var err error
baseRawServices, err = options.Preprocess(baseRawServices)
if err != nil {
return nil, err
}
}
if options.Validate {
if err := validate(baseRawServices); err != nil {
return nil, err
}
}
baseService, ok = baseRawServices[service]
if !ok {
return nil, fmt.Errorf("Failed to find service %s in file %s", service, file)
}
baseService, err = parseV1(resourceLookup, environmentLookup, resolved, baseService, baseRawServices, options)
}
if err != nil {
return nil, err
}
baseService = clone(baseService)
logrus.Debugf("Merging %#v, %#v", baseService, serviceData)
for _, k := range noMerge {
if _, ok := baseService[k]; ok {
source := file
if source == "" {
source = inFile
}
return nil, fmt.Errorf("Cannot extend service '%s' in %s: services with '%s' cannot be extended", service, source, k)
}
}
baseService = mergeConfig(baseService, serviceData)
logrus.Debugf("Merged result %#v", baseService)
return baseService, nil
}
func resolveContextV1(inFile string, serviceData RawService) RawService {
context := asString(serviceData["build"])
if context == "" {
return serviceData
}
if IsValidRemote(context) {
return serviceData
}
current := path.Dir(inFile)
if context == "." {
context = current
} else {
current = path.Join(current, context)
}
serviceData["build"] = current
return serviceData
}

View file

@ -0,0 +1,217 @@
package config
import (
"fmt"
"path"
"github.com/Sirupsen/logrus"
yaml "github.com/cloudfoundry-incubator/candiedyaml"
"github.com/docker/libcompose/utils"
)
// MergeServicesV2 merges a v2 compose file into an existing set of service configs
func MergeServicesV2(existingServices *ServiceConfigs, environmentLookup EnvironmentLookup, resourceLookup ResourceLookup, file string, bytes []byte, options *ParseOptions) (map[string]*ServiceConfig, error) {
var config Config
if err := yaml.Unmarshal(bytes, &config); err != nil {
return nil, err
}
datas := config.Services
if options.Interpolate {
if err := Interpolate(environmentLookup, &datas); err != nil {
return nil, err
}
}
if options.Preprocess != nil {
var err error
datas, err = options.Preprocess(datas)
if err != nil {
return nil, err
}
}
for name, data := range datas {
data, err := parseV2(resourceLookup, environmentLookup, file, data, datas, options)
if err != nil {
logrus.Errorf("Failed to parse service %s: %v", name, err)
return nil, err
}
if serviceConfig, ok := existingServices.Get(name); ok {
var rawExistingService RawService
if err := utils.Convert(serviceConfig, &rawExistingService); err != nil {
return nil, err
}
data = mergeConfig(rawExistingService, data)
}
datas[name] = data
}
serviceConfigs := make(map[string]*ServiceConfig)
if err := utils.Convert(datas, &serviceConfigs); err != nil {
return nil, err
}
return serviceConfigs, nil
}
// ParseVolumes parses volumes in a compose file
func ParseVolumes(bytes []byte) (map[string]*VolumeConfig, error) {
volumeConfigs := make(map[string]*VolumeConfig)
var config Config
if err := yaml.Unmarshal(bytes, &config); err != nil {
return nil, err
}
if err := utils.Convert(config.Volumes, &volumeConfigs); err != nil {
return nil, err
}
return volumeConfigs, nil
}
// ParseNetworks parses networks in a compose file
func ParseNetworks(bytes []byte) (map[string]*NetworkConfig, error) {
networkConfigs := make(map[string]*NetworkConfig)
var config Config
if err := yaml.Unmarshal(bytes, &config); err != nil {
return nil, err
}
if err := utils.Convert(config.Networks, &networkConfigs); err != nil {
return nil, err
}
return networkConfigs, nil
}
func parseV2(resourceLookup ResourceLookup, environmentLookup EnvironmentLookup, inFile string, serviceData RawService, datas RawServiceMap, options *ParseOptions) (RawService, error) {
serviceData, err := readEnvFile(resourceLookup, inFile, serviceData)
if err != nil {
return nil, err
}
serviceData = resolveContextV2(inFile, serviceData)
value, ok := serviceData["extends"]
if !ok {
return serviceData, nil
}
mapValue, ok := value.(map[interface{}]interface{})
if !ok {
return serviceData, nil
}
if resourceLookup == nil {
return nil, fmt.Errorf("Can not use extends in file %s no mechanism provided to files", inFile)
}
file := asString(mapValue["file"])
service := asString(mapValue["service"])
if service == "" {
return serviceData, nil
}
var baseService RawService
if file == "" {
if serviceData, ok := datas[service]; ok {
baseService, err = parseV2(resourceLookup, environmentLookup, inFile, serviceData, datas, options)
} else {
return nil, fmt.Errorf("Failed to find service %s to extend", service)
}
} else {
bytes, resolved, err := resourceLookup.Lookup(file, inFile)
if err != nil {
logrus.Errorf("Failed to lookup file %s: %v", file, err)
return nil, err
}
var config Config
if err := yaml.Unmarshal(bytes, &config); err != nil {
return nil, err
}
baseRawServices := config.Services
if options.Interpolate {
err = Interpolate(environmentLookup, &baseRawServices)
if err != nil {
return nil, err
}
}
baseService, ok = baseRawServices[service]
if !ok {
return nil, fmt.Errorf("Failed to find service %s in file %s", service, file)
}
baseService, err = parseV2(resourceLookup, environmentLookup, resolved, baseService, baseRawServices, options)
}
if err != nil {
return nil, err
}
baseService = clone(baseService)
logrus.Debugf("Merging %#v, %#v", baseService, serviceData)
for _, k := range noMerge {
if _, ok := baseService[k]; ok {
source := file
if source == "" {
source = inFile
}
return nil, fmt.Errorf("Cannot extend service '%s' in %s: services with '%s' cannot be extended", service, source, k)
}
}
baseService = mergeConfig(baseService, serviceData)
logrus.Debugf("Merged result %#v", baseService)
return baseService, nil
}
func resolveContextV2(inFile string, serviceData RawService) RawService {
if _, ok := serviceData["build"]; !ok {
return serviceData
}
var build map[interface{}]interface{}
if buildAsString, ok := serviceData["build"].(string); ok {
build = map[interface{}]interface{}{
"context": buildAsString,
}
} else {
build = serviceData["build"].(map[interface{}]interface{})
}
context := asString(build["context"])
if context == "" {
return serviceData
}
if IsValidRemote(context) {
return serviceData
}
current := path.Dir(inFile)
if context == "." {
context = current
} else {
current = path.Join(current, context)
}
build["context"] = current
return serviceData
}

View file

@ -0,0 +1,510 @@
package config
var schemaV1 = `{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "config_schema_v1.json",
"type": "object",
"patternProperties": {
"^[a-zA-Z0-9._-]+$": {
"$ref": "#/definitions/service"
}
},
"additionalProperties": false,
"definitions": {
"service": {
"id": "#/definitions/service",
"type": "object",
"properties": {
"build": {"type": "string"},
"cap_add": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"cap_drop": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"cgroup_parent": {"type": "string"},
"command": {
"oneOf": [
{"type": "string"},
{"type": "array", "items": {"type": "string"}}
]
},
"container_name": {"type": "string"},
"cpu_shares": {"type": ["number", "string"]},
"cpu_quota": {"type": ["number", "string"]},
"cpuset": {"type": "string"},
"devices": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"dns": {"$ref": "#/definitions/string_or_list"},
"dns_search": {"$ref": "#/definitions/string_or_list"},
"dockerfile": {"type": "string"},
"domainname": {"type": "string"},
"entrypoint": {
"oneOf": [
{"type": "string"},
{"type": "array", "items": {"type": "string"}}
]
},
"env_file": {"$ref": "#/definitions/string_or_list"},
"environment": {"$ref": "#/definitions/list_or_dict"},
"expose": {
"type": "array",
"items": {
"type": ["string", "number"],
"format": "expose"
},
"uniqueItems": true
},
"extends": {
"oneOf": [
{
"type": "string"
},
{
"type": "object",
"properties": {
"service": {"type": "string"},
"file": {"type": "string"}
},
"required": ["service"],
"additionalProperties": false
}
]
},
"extra_hosts": {"$ref": "#/definitions/list_or_dict"},
"external_links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"hostname": {"type": "string"},
"image": {"type": "string"},
"ipc": {"type": "string"},
"labels": {"$ref": "#/definitions/list_or_dict"},
"links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"log_driver": {"type": "string"},
"log_opt": {"type": "object"},
"mac_address": {"type": "string"},
"mem_limit": {"type": ["number", "string"]},
"memswap_limit": {"type": ["number", "string"]},
"net": {"type": "string"},
"pid": {"type": ["string", "null"]},
"ports": {
"type": "array",
"items": {
"type": ["string", "number"],
"format": "ports"
},
"uniqueItems": true
},
"privileged": {"type": "boolean"},
"read_only": {"type": "boolean"},
"restart": {"type": "string"},
"security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"shm_size": {"type": ["number", "string"]},
"stdin_open": {"type": "boolean"},
"stop_signal": {"type": "string"},
"tty": {"type": "boolean"},
"ulimits": {
"type": "object",
"patternProperties": {
"^[a-z]+$": {
"oneOf": [
{"type": "integer"},
{
"type":"object",
"properties": {
"hard": {"type": "integer"},
"soft": {"type": "integer"}
},
"required": ["soft", "hard"],
"additionalProperties": false
}
]
}
}
},
"user": {"type": "string"},
"volumes": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"volume_driver": {"type": "string"},
"volumes_from": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"working_dir": {"type": "string"}
},
"dependencies": {
"memswap_limit": ["mem_limit"]
},
"additionalProperties": false
},
"string_or_list": {
"oneOf": [
{"type": "string"},
{"$ref": "#/definitions/list_of_strings"}
]
},
"list_of_strings": {
"type": "array",
"items": {"type": "string"},
"uniqueItems": true
},
"list_or_dict": {
"oneOf": [
{
"type": "object",
"patternProperties": {
".+": {
"type": ["string", "number", "null"]
}
},
"additionalProperties": false
},
{"type": "array", "items": {"type": "string"}, "uniqueItems": true}
]
},
"constraints": {
"service": {
"id": "#/definitions/constraints/service",
"anyOf": [
{
"required": ["build"],
"not": {"required": ["image"]}
},
{
"required": ["image"],
"not": {"anyOf": [
{"required": ["build"]},
{"required": ["dockerfile"]}
]}
}
]
}
}
}
}
`
var schemaV2 = `{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "config_schema_v2.0.json",
"type": "object",
"properties": {
"version": {
"type": "string"
},
"services": {
"id": "#/properties/services",
"type": "object",
"patternProperties": {
"^[a-zA-Z0-9._-]+$": {
"$ref": "#/definitions/service"
}
},
"additionalProperties": false
},
"networks": {
"id": "#/properties/networks",
"type": "object",
"patternProperties": {
"^[a-zA-Z0-9._-]+$": {
"$ref": "#/definitions/network"
}
}
},
"volumes": {
"id": "#/properties/volumes",
"type": "object",
"patternProperties": {
"^[a-zA-Z0-9._-]+$": {
"$ref": "#/definitions/volume"
}
},
"additionalProperties": false
}
},
"additionalProperties": false,
"definitions": {
"service": {
"id": "#/definitions/service",
"type": "object",
"properties": {
"build": {
"oneOf": [
{"type": "string"},
{
"type": "object",
"properties": {
"context": {"type": "string"},
"dockerfile": {"type": "string"},
"args": {"$ref": "#/definitions/list_or_dict"}
},
"additionalProperties": false
}
]
},
"cap_add": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"cap_drop": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"cgroup_parent": {"type": "string"},
"command": {
"oneOf": [
{"type": "string"},
{"type": "array", "items": {"type": "string"}}
]
},
"container_name": {"type": "string"},
"cpu_shares": {"type": ["number", "string"]},
"cpu_quota": {"type": ["number", "string"]},
"cpuset": {"type": "string"},
"depends_on": {"$ref": "#/definitions/list_of_strings"},
"devices": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"dns": {"$ref": "#/definitions/string_or_list"},
"dns_search": {"$ref": "#/definitions/string_or_list"},
"domainname": {"type": "string"},
"entrypoint": {
"oneOf": [
{"type": "string"},
{"type": "array", "items": {"type": "string"}}
]
},
"env_file": {"$ref": "#/definitions/string_or_list"},
"environment": {"$ref": "#/definitions/list_or_dict"},
"expose": {
"type": "array",
"items": {
"type": ["string", "number"],
"format": "expose"
},
"uniqueItems": true
},
"extends": {
"oneOf": [
{
"type": "string"
},
{
"type": "object",
"properties": {
"service": {"type": "string"},
"file": {"type": "string"}
},
"required": ["service"],
"additionalProperties": false
}
]
},
"external_links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"extra_hosts": {"$ref": "#/definitions/list_or_dict"},
"hostname": {"type": "string"},
"image": {"type": "string"},
"ipc": {"type": "string"},
"labels": {"$ref": "#/definitions/list_or_dict"},
"links": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"logging": {
"type": "object",
"properties": {
"driver": {"type": "string"},
"options": {"type": "object"}
},
"additionalProperties": false
},
"mac_address": {"type": "string"},
"mem_limit": {"type": ["number", "string"]},
"memswap_limit": {"type": ["number", "string"]},
"network_mode": {"type": "string"},
"networks": {
"oneOf": [
{"$ref": "#/definitions/list_of_strings"},
{
"type": "object",
"patternProperties": {
"^[a-zA-Z0-9._-]+$": {
"oneOf": [
{
"type": "object",
"properties": {
"aliases": {"$ref": "#/definitions/list_of_strings"},
"ipv4_address": {"type": "string"},
"ipv6_address": {"type": "string"}
},
"additionalProperties": false
},
{"type": "null"}
]
}
},
"additionalProperties": false
}
]
},
"pid": {"type": ["string", "null"]},
"ports": {
"type": "array",
"items": {
"type": ["string", "number"],
"format": "ports"
},
"uniqueItems": true
},
"privileged": {"type": "boolean"},
"read_only": {"type": "boolean"},
"restart": {"type": "string"},
"security_opt": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"shm_size": {"type": ["number", "string"]},
"stdin_open": {"type": "boolean"},
"stop_signal": {"type": "string"},
"tmpfs": {"$ref": "#/definitions/string_or_list"},
"tty": {"type": "boolean"},
"ulimits": {
"type": "object",
"patternProperties": {
"^[a-z]+$": {
"oneOf": [
{"type": "integer"},
{
"type":"object",
"properties": {
"hard": {"type": "integer"},
"soft": {"type": "integer"}
},
"required": ["soft", "hard"],
"additionalProperties": false
}
]
}
}
},
"user": {"type": "string"},
"volumes": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"volume_driver": {"type": "string"},
"volumes_from": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
"working_dir": {"type": "string"}
},
"dependencies": {
"memswap_limit": ["mem_limit"]
},
"additionalProperties": false
},
"network": {
"id": "#/definitions/network",
"type": "object",
"properties": {
"driver": {"type": "string"},
"driver_opts": {
"type": "object",
"patternProperties": {
"^.+$": {"type": ["string", "number"]}
}
},
"ipam": {
"type": "object",
"properties": {
"driver": {"type": "string"},
"config": {
"type": "array"
}
},
"additionalProperties": false
},
"external": {
"type": ["boolean", "object"],
"properties": {
"name": {"type": "string"}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
"volume": {
"id": "#/definitions/volume",
"type": ["object", "null"],
"properties": {
"driver": {"type": "string"},
"driver_opts": {
"type": "object",
"patternProperties": {
"^.+$": {"type": ["string", "number"]}
}
},
"external": {
"type": ["boolean", "object"],
"properties": {
"name": {"type": "string"}
}
},
"additionalProperties": false
},
"additionalProperties": false
},
"string_or_list": {
"oneOf": [
{"type": "string"},
{"$ref": "#/definitions/list_of_strings"}
]
},
"list_of_strings": {
"type": "array",
"items": {"type": "string"},
"uniqueItems": true
},
"list_or_dict": {
"oneOf": [
{
"type": "object",
"patternProperties": {
".+": {
"type": ["string", "number", "null"]
}
},
"additionalProperties": false
},
{"type": "array", "items": {"type": "string"}, "uniqueItems": true}
]
},
"constraints": {
"service": {
"id": "#/definitions/constraints/service",
"anyOf": [
{"required": ["build"]},
{"required": ["image"]}
],
"properties": {
"build": {
"required": ["context"]
}
}
}
}
}
}
`

View file

@ -0,0 +1,92 @@
package config
import (
"encoding/json"
"strings"
"github.com/docker/go-connections/nat"
"github.com/xeipuuv/gojsonschema"
)
var (
schemaLoader gojsonschema.JSONLoader
constraintSchemaLoader gojsonschema.JSONLoader
schema map[string]interface{}
)
type (
environmentFormatChecker struct{}
portsFormatChecker struct{}
)
func (checker environmentFormatChecker) IsFormat(input string) bool {
// If the value is a boolean, a warning should be given
// However, we can't determine type since gojsonschema converts the value to a string
// Adding a function with an interface{} parameter to gojsonschema is probably the best way to handle this
return true
}
func (checker portsFormatChecker) IsFormat(input string) bool {
_, _, err := nat.ParsePortSpecs([]string{input})
return err == nil
}
func setupSchemaLoaders() error {
if schema != nil {
return nil
}
var schemaRaw interface{}
err := json.Unmarshal([]byte(schemaV1), &schemaRaw)
if err != nil {
return err
}
schema = schemaRaw.(map[string]interface{})
gojsonschema.FormatCheckers.Add("environment", environmentFormatChecker{})
gojsonschema.FormatCheckers.Add("ports", portsFormatChecker{})
gojsonschema.FormatCheckers.Add("expose", portsFormatChecker{})
schemaLoader = gojsonschema.NewGoLoader(schemaRaw)
definitions := schema["definitions"].(map[string]interface{})
constraints := definitions["constraints"].(map[string]interface{})
service := constraints["service"].(map[string]interface{})
constraintSchemaLoader = gojsonschema.NewGoLoader(service)
return nil
}
// gojsonschema doesn't provide a list of valid types for a property
// This parses the schema manually to find all valid types
func parseValidTypesFromSchema(schema map[string]interface{}, context string) []string {
contextSplit := strings.Split(context, ".")
key := contextSplit[len(contextSplit)-1]
definitions := schema["definitions"].(map[string]interface{})
service := definitions["service"].(map[string]interface{})
properties := service["properties"].(map[string]interface{})
property := properties[key].(map[string]interface{})
var validTypes []string
if val, ok := property["oneOf"]; ok {
validConditions := val.([]interface{})
for _, validCondition := range validConditions {
condition := validCondition.(map[string]interface{})
validTypes = append(validTypes, condition["type"].(string))
}
} else if val, ok := property["$ref"]; ok {
reference := val.(string)
if reference == "#/definitions/string_or_list" {
return []string{"string", "array"}
} else if reference == "#/definitions/list_of_strings" {
return []string{"array"}
} else if reference == "#/definitions/list_or_dict" {
return []string{"array", "object"}
}
}
return validTypes
}

View file

@ -0,0 +1,234 @@
package config
import (
"sync"
"github.com/docker/libcompose/yaml"
)
// EnvironmentLookup defines methods to provides environment variable loading.
type EnvironmentLookup interface {
Lookup(key, serviceName string, config *ServiceConfig) []string
}
// ResourceLookup defines methods to provides file loading.
type ResourceLookup interface {
Lookup(file, relativeTo string) ([]byte, string, error)
ResolvePath(path, inFile string) string
}
// ServiceConfigV1 holds version 1 of libcompose service configuration
type ServiceConfigV1 struct {
Build string `yaml:"build,omitempty"`
CapAdd []string `yaml:"cap_add,omitempty"`
CapDrop []string `yaml:"cap_drop,omitempty"`
CgroupParent string `yaml:"cgroup_parent,omitempty"`
CPUQuota int64 `yaml:"cpu_quota,omitempty"`
CPUSet string `yaml:"cpuset,omitempty"`
CPUShares int64 `yaml:"cpu_shares,omitempty"`
Command yaml.Command `yaml:"command,flow,omitempty"`
ContainerName string `yaml:"container_name,omitempty"`
Devices []string `yaml:"devices,omitempty"`
DNS yaml.Stringorslice `yaml:"dns,omitempty"`
DNSSearch yaml.Stringorslice `yaml:"dns_search,omitempty"`
Dockerfile string `yaml:"dockerfile,omitempty"`
DomainName string `yaml:"domainname,omitempty"`
Entrypoint yaml.Command `yaml:"entrypoint,flow,omitempty"`
EnvFile yaml.Stringorslice `yaml:"env_file,omitempty"`
Environment yaml.MaporEqualSlice `yaml:"environment,omitempty"`
Hostname string `yaml:"hostname,omitempty"`
Image string `yaml:"image,omitempty"`
Labels yaml.SliceorMap `yaml:"labels,omitempty"`
Links yaml.MaporColonSlice `yaml:"links,omitempty"`
LogDriver string `yaml:"log_driver,omitempty"`
MacAddress string `yaml:"mac_address,omitempty"`
MemLimit int64 `yaml:"mem_limit,omitempty"`
MemSwapLimit int64 `yaml:"memswap_limit,omitempty"`
Name string `yaml:"name,omitempty"`
Net string `yaml:"net,omitempty"`
Pid string `yaml:"pid,omitempty"`
Uts string `yaml:"uts,omitempty"`
Ipc string `yaml:"ipc,omitempty"`
Ports []string `yaml:"ports,omitempty"`
Privileged bool `yaml:"privileged,omitempty"`
Restart string `yaml:"restart,omitempty"`
ReadOnly bool `yaml:"read_only,omitempty"`
ShmSize int64 `yaml:"shm_size,omitempty"`
StdinOpen bool `yaml:"stdin_open,omitempty"`
SecurityOpt []string `yaml:"security_opt,omitempty"`
Tty bool `yaml:"tty,omitempty"`
User string `yaml:"user,omitempty"`
VolumeDriver string `yaml:"volume_driver,omitempty"`
Volumes []string `yaml:"volumes,omitempty"`
VolumesFrom []string `yaml:"volumes_from,omitempty"`
WorkingDir string `yaml:"working_dir,omitempty"`
Expose []string `yaml:"expose,omitempty"`
ExternalLinks []string `yaml:"external_links,omitempty"`
LogOpt map[string]string `yaml:"log_opt,omitempty"`
ExtraHosts []string `yaml:"extra_hosts,omitempty"`
Ulimits yaml.Ulimits `yaml:"ulimits,omitempty"`
}
// Log holds v2 logging information
type Log struct {
Driver string `yaml:"driver,omitempty"`
Options map[string]string `yaml:"options,omitempty"`
}
// ServiceConfig holds version 2 of libcompose service configuration
type ServiceConfig struct {
Build yaml.Build `yaml:"build,omitempty"`
CapAdd []string `yaml:"cap_add,omitempty"`
CapDrop []string `yaml:"cap_drop,omitempty"`
CPUSet string `yaml:"cpuset,omitempty"`
CPUShares int64 `yaml:"cpu_shares,omitempty"`
CPUQuota int64 `yaml:"cpu_quota,omitempty"`
Command yaml.Command `yaml:"command,flow,omitempty"`
CgroupParent string `yaml:"cgroup_parent,omitempty"`
ContainerName string `yaml:"container_name,omitempty"`
Devices []string `yaml:"devices,omitempty"`
DependsOn []string `yaml:"depends_on,omitempty"`
DNS yaml.Stringorslice `yaml:"dns,omitempty"`
DNSSearch yaml.Stringorslice `yaml:"dns_search,omitempty"`
DomainName string `yaml:"domain_name,omitempty"`
Entrypoint yaml.Command `yaml:"entrypoint,flow,omitempty"`
EnvFile yaml.Stringorslice `yaml:"env_file,omitempty"`
Environment yaml.MaporEqualSlice `yaml:"environment,omitempty"`
Expose []string `yaml:"expose,omitempty"`
Extends yaml.MaporEqualSlice `yaml:"extends,omitempty"`
ExternalLinks []string `yaml:"external_links,omitempty"`
ExtraHosts []string `yaml:"extra_hosts,omitempty"`
Image string `yaml:"image,omitempty"`
Hostname string `yaml:"hostname,omitempty"`
Ipc string `yaml:"ipc,omitempty"`
Labels yaml.SliceorMap `yaml:"labels,omitempty"`
Links yaml.MaporColonSlice `yaml:"links,omitempty"`
Logging Log `yaml:"logging,omitempty"`
MacAddress string `yaml:"mac_address,omitempty"`
MemLimit int64 `yaml:"mem_limit,omitempty"`
MemSwapLimit int64 `yaml:"memswap_limit,omitempty"`
NetworkMode string `yaml:"network_mode,omitempty"`
Networks *yaml.Networks `yaml:"networks,omitempty"`
Pid string `yaml:"pid,omitempty"`
Ports []string `yaml:"ports,omitempty"`
Privileged bool `yaml:"privileged,omitempty"`
SecurityOpt []string `yaml:"security_opt,omitempty"`
ShmSize int64 `yaml:"shm_size,omitempty"`
StopSignal string `yaml:"stop_signal,omitempty"`
VolumeDriver string `yaml:"volume_driver,omitempty"`
Volumes []string `yaml:"volumes,omitempty"`
VolumesFrom []string `yaml:"volumes_from,omitempty"`
Uts string `yaml:"uts,omitempty"`
Restart string `yaml:"restart,omitempty"`
ReadOnly bool `yaml:"read_only,omitempty"`
StdinOpen bool `yaml:"stdin_open,omitempty"`
Tty bool `yaml:"tty,omitempty"`
User string `yaml:"user,omitempty"`
WorkingDir string `yaml:"working_dir,omitempty"`
Ulimits yaml.Ulimits `yaml:"ulimits,omitempty"`
}
// VolumeConfig holds v2 volume configuration
type VolumeConfig struct {
Driver string `yaml:"driver,omitempty"`
DriverOpts map[string]string `yaml:"driver_opts,omitempty"`
External yaml.External `yaml:"external,omitempty"`
}
// Ipam holds v2 network IPAM information
type Ipam struct {
Driver string `yaml:"driver,omitempty"`
Config []IpamConfig `yaml:"config,omitempty"`
}
// IpamConfig holds v2 network IPAM configuration information
type IpamConfig struct {
Subnet string `yaml:"subnet,omitempty"`
IPRange string `yaml:"ip_range,omitempty"`
Gateway string `yaml:"gateway,omitempty"`
AuxAddress map[string]string `yaml:"aux_addresses,omitempty"`
}
// NetworkConfig holds v2 network configuration
type NetworkConfig struct {
Driver string `yaml:"driver,omitempty"`
DriverOpts map[string]string `yaml:"driver_opts,omitempty"`
External yaml.External `yaml:"external,omitempty"`
Ipam Ipam `yaml:"ipam,omitempty"`
}
// Config holds libcompose top level configuration
type Config struct {
Version string `yaml:"version,omitempty"`
Services RawServiceMap `yaml:"services,omitempty"`
Volumes map[string]*VolumeConfig `yaml:"volumes,omitempty"`
Networks map[string]*NetworkConfig `yaml:"networks,omitempty"`
}
// NewServiceConfigs initializes a new Configs struct
func NewServiceConfigs() *ServiceConfigs {
return &ServiceConfigs{
m: make(map[string]*ServiceConfig),
}
}
// ServiceConfigs holds a concurrent safe map of ServiceConfig
type ServiceConfigs struct {
m map[string]*ServiceConfig
mu sync.RWMutex
}
// Has checks if the config map has the specified name
func (c *ServiceConfigs) Has(name string) bool {
c.mu.RLock()
defer c.mu.RUnlock()
_, ok := c.m[name]
return ok
}
// Get returns the config and the presence of the specified name
func (c *ServiceConfigs) Get(name string) (*ServiceConfig, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
service, ok := c.m[name]
return service, ok
}
// Add add the specifed config with the specified name
func (c *ServiceConfigs) Add(name string, service *ServiceConfig) {
c.mu.Lock()
c.m[name] = service
c.mu.Unlock()
}
// Len returns the len of the configs
func (c *ServiceConfigs) Len() int {
c.mu.RLock()
defer c.mu.RUnlock()
return len(c.m)
}
// Keys returns the names of the config
func (c *ServiceConfigs) Keys() []string {
keys := []string{}
c.mu.RLock()
defer c.mu.RUnlock()
for name := range c.m {
keys = append(keys, name)
}
return keys
}
// RawService is represent a Service in map form unparsed
type RawService map[string]interface{}
// RawServiceMap is a collection of RawServices
type RawServiceMap map[string]RawService
// ParseOptions are a set of options to customize the parsing process
type ParseOptions struct {
Interpolate bool
Validate bool
Preprocess func(RawServiceMap) (RawServiceMap, error)
Postprocess func(map[string]*ServiceConfig) (map[string]*ServiceConfig, error)
}

View file

@ -0,0 +1,42 @@
package config
func merge(existing, value interface{}) interface{} {
// append strings
if left, lok := existing.([]interface{}); lok {
if right, rok := value.([]interface{}); rok {
return append(left, right...)
}
}
//merge maps
if left, lok := existing.(map[interface{}]interface{}); lok {
if right, rok := value.(map[interface{}]interface{}); rok {
newLeft := make(map[interface{}]interface{})
for k, v := range left {
newLeft[k] = v
}
for k, v := range right {
newLeft[k] = v
}
return newLeft
}
}
return value
}
func clone(in RawService) RawService {
result := RawService{}
for k, v := range in {
result[k] = v
}
return result
}
func asString(obj interface{}) string {
if v, ok := obj.(string); ok {
return v
}
return ""
}

View file

@ -0,0 +1,300 @@
package config
import (
"fmt"
"strconv"
"strings"
"github.com/xeipuuv/gojsonschema"
)
func serviceNameFromErrorField(field string) string {
splitKeys := strings.Split(field, ".")
return splitKeys[0]
}
func keyNameFromErrorField(field string) string {
splitKeys := strings.Split(field, ".")
if len(splitKeys) > 0 {
return splitKeys[len(splitKeys)-1]
}
return ""
}
func containsTypeError(resultError gojsonschema.ResultError) bool {
contextSplit := strings.Split(resultError.Context().String(), ".")
_, err := strconv.Atoi(contextSplit[len(contextSplit)-1])
return err == nil
}
func addArticle(s string) string {
switch s[0] {
case 'a', 'e', 'i', 'o', 'u', 'A', 'E', 'I', 'O', 'U':
return "an " + s
default:
return "a " + s
}
}
// Gets the value in a service map at a given error context
func getValue(val interface{}, context string) string {
keys := strings.Split(context, ".")
if keys[0] == "(root)" {
keys = keys[1:]
}
for i, k := range keys {
switch typedVal := (val).(type) {
case string:
return typedVal
case []interface{}:
if index, err := strconv.Atoi(k); err == nil {
val = typedVal[index]
}
case RawServiceMap:
val = typedVal[k]
case RawService:
val = typedVal[k]
case map[interface{}]interface{}:
val = typedVal[k]
}
if i == len(keys)-1 {
return fmt.Sprint(val)
}
}
return ""
}
// Converts map[interface{}]interface{} to map[string]interface{} recursively
// gojsonschema only accepts map[string]interface{}
func convertServiceMapKeysToStrings(serviceMap RawServiceMap) RawServiceMap {
newServiceMap := make(RawServiceMap)
for k, v := range serviceMap {
newServiceMap[k] = convertServiceKeysToStrings(v)
}
return newServiceMap
}
func convertServiceKeysToStrings(service RawService) RawService {
newService := make(RawService)
for k, v := range service {
newService[k] = convertKeysToStrings(v)
}
return newService
}
func convertKeysToStrings(item interface{}) interface{} {
switch typedDatas := item.(type) {
case map[interface{}]interface{}:
newMap := make(map[string]interface{})
for key, value := range typedDatas {
stringKey := key.(string)
newMap[stringKey] = convertKeysToStrings(value)
}
return newMap
case []interface{}:
// newArray := make([]interface{}, 0) will cause golint to complain
var newArray []interface{}
newArray = make([]interface{}, 0)
for _, value := range typedDatas {
newArray = append(newArray, convertKeysToStrings(value))
}
return newArray
default:
return item
}
}
var dockerConfigHints = map[string]string{
"cpu_share": "cpu_shares",
"add_host": "extra_hosts",
"hosts": "extra_hosts",
"extra_host": "extra_hosts",
"device": "devices",
"link": "links",
"memory_swap": "memswap_limit",
"port": "ports",
"privilege": "privileged",
"priviliged": "privileged",
"privilige": "privileged",
"volume": "volumes",
"workdir": "working_dir",
}
func unsupportedConfigMessage(key string, nextErr gojsonschema.ResultError) string {
service := serviceNameFromErrorField(nextErr.Field())
message := fmt.Sprintf("Unsupported config option for %s service: '%s'", service, key)
if val, ok := dockerConfigHints[key]; ok {
message += fmt.Sprintf(" (did you mean '%s'?)", val)
}
return message
}
func oneOfMessage(serviceMap RawServiceMap, schema map[string]interface{}, err, nextErr gojsonschema.ResultError) string {
switch nextErr.Type() {
case "additional_property_not_allowed":
property := nextErr.Details()["property"]
return fmt.Sprintf("contains unsupported option: '%s'", property)
case "invalid_type":
if containsTypeError(nextErr) {
expectedType := addArticle(nextErr.Details()["expected"].(string))
return fmt.Sprintf("contains %s, which is an invalid type, it should be %s", getValue(serviceMap, nextErr.Context().String()), expectedType)
}
validTypes := parseValidTypesFromSchema(schema, err.Context().String())
validTypesMsg := addArticle(strings.Join(validTypes, " or "))
return fmt.Sprintf("contains an invalid type, it should be %s", validTypesMsg)
case "unique":
contextWithDuplicates := getValue(serviceMap, nextErr.Context().String())
return fmt.Sprintf("contains non unique items, please remove duplicates from %s", contextWithDuplicates)
}
return ""
}
func invalidTypeMessage(service, key string, err gojsonschema.ResultError) string {
expectedTypesString := err.Details()["expected"].(string)
var expectedTypes []string
if strings.Contains(expectedTypesString, ",") {
expectedTypes = strings.Split(expectedTypesString[1:len(expectedTypesString)-1], ",")
} else {
expectedTypes = []string{expectedTypesString}
}
validTypesMsg := addArticle(strings.Join(expectedTypes, " or "))
return fmt.Sprintf("Service '%s' configuration key '%s' contains an invalid type, it should be %s.", service, key, validTypesMsg)
}
func validate(serviceMap RawServiceMap) error {
if err := setupSchemaLoaders(); err != nil {
return err
}
serviceMap = convertServiceMapKeysToStrings(serviceMap)
var validationErrors []string
dataLoader := gojsonschema.NewGoLoader(serviceMap)
result, err := gojsonschema.Validate(schemaLoader, dataLoader)
if err != nil {
return err
}
// gojsonschema can create extraneous "additional_property_not_allowed" errors in some cases
// If this is set, and the error is at root level, skip over that error
skipRootAdditionalPropertyError := false
if !result.Valid() {
for i := 0; i < len(result.Errors()); i++ {
err := result.Errors()[i]
if skipRootAdditionalPropertyError && err.Type() == "additional_property_not_allowed" && err.Context().String() == "(root)" {
skipRootAdditionalPropertyError = false
continue
}
if err.Context().String() == "(root)" {
switch err.Type() {
case "additional_property_not_allowed":
validationErrors = append(validationErrors, fmt.Sprintf("Invalid service name '%s' - only [a-zA-Z0-9\\._\\-] characters are allowed", err.Field()))
default:
validationErrors = append(validationErrors, err.Description())
}
} else {
skipRootAdditionalPropertyError = true
serviceName := serviceNameFromErrorField(err.Field())
key := keyNameFromErrorField(err.Field())
switch err.Type() {
case "additional_property_not_allowed":
validationErrors = append(validationErrors, unsupportedConfigMessage(key, result.Errors()[i+1]))
case "number_one_of":
validationErrors = append(validationErrors, fmt.Sprintf("Service '%s' configuration key '%s' %s", serviceName, key, oneOfMessage(serviceMap, schema, err, result.Errors()[i+1])))
// Next error handled in oneOfMessage, skip over it
i++
case "invalid_type":
validationErrors = append(validationErrors, invalidTypeMessage(serviceName, key, err))
case "required":
validationErrors = append(validationErrors, fmt.Sprintf("Service '%s' option '%s' is invalid, %s", serviceName, key, err.Description()))
case "missing_dependency":
dependency := err.Details()["dependency"].(string)
validationErrors = append(validationErrors, fmt.Sprintf("Invalid configuration for '%s' service: dependency '%s' is not satisfied", serviceName, dependency))
case "unique":
contextWithDuplicates := getValue(serviceMap, err.Context().String())
validationErrors = append(validationErrors, fmt.Sprintf("Service '%s' configuration key '%s' value %s has non-unique elements", serviceName, key, contextWithDuplicates))
default:
validationErrors = append(validationErrors, fmt.Sprintf("Service '%s' configuration key %s value %s", serviceName, key, err.Description()))
}
}
}
return fmt.Errorf(strings.Join(validationErrors, "\n"))
}
return nil
}
func validateServiceConstraints(service RawService, serviceName string) error {
if err := setupSchemaLoaders(); err != nil {
return err
}
service = convertServiceKeysToStrings(service)
var validationErrors []string
dataLoader := gojsonschema.NewGoLoader(service)
result, err := gojsonschema.Validate(constraintSchemaLoader, dataLoader)
if err != nil {
return err
}
if !result.Valid() {
for _, err := range result.Errors() {
if err.Type() == "number_any_of" {
_, containsImage := service["image"]
_, containsBuild := service["build"]
_, containsDockerfile := service["dockerfile"]
if containsImage && containsBuild {
validationErrors = append(validationErrors, fmt.Sprintf("Service '%s' has both an image and build path specified. A service can either be built to image or use an existing image, not both.", serviceName))
} else if !containsImage && !containsBuild {
validationErrors = append(validationErrors, fmt.Sprintf("Service '%s' has neither an image nor a build path specified. Exactly one must be provided.", serviceName))
} else if containsImage && containsDockerfile {
validationErrors = append(validationErrors, fmt.Sprintf("Service '%s' has both an image and alternate Dockerfile. A service can either be built to image or use an existing image, not both.", serviceName))
}
}
}
return fmt.Errorf(strings.Join(validationErrors, "\n"))
}
return nil
}

View file

@ -0,0 +1,40 @@
package docker
import (
"github.com/docker/docker/registry"
"github.com/docker/engine-api/types"
)
// AuthLookup defines a method for looking up authentication information
type AuthLookup interface {
All() map[string]types.AuthConfig
Lookup(repoInfo *registry.RepositoryInfo) types.AuthConfig
}
// ConfigAuthLookup implements AuthLookup by reading a Docker config file
type ConfigAuthLookup struct {
context *Context
}
// NewConfigAuthLookup creates a new ConfigAuthLookup for a given context
func NewConfigAuthLookup(context *Context) *ConfigAuthLookup {
return &ConfigAuthLookup{
context: context,
}
}
// Lookup uses a Docker config file to lookup authentication information
func (c *ConfigAuthLookup) Lookup(repoInfo *registry.RepositoryInfo) types.AuthConfig {
if c.context.ConfigFile == nil || repoInfo == nil || repoInfo.Index == nil {
return types.AuthConfig{}
}
return registry.ResolveAuthConfig(c.context.ConfigFile.AuthConfigs, repoInfo.Index)
}
// All uses a Docker config file to get all authentication information
func (c *ConfigAuthLookup) All() map[string]types.AuthConfig {
if c.context.ConfigFile == nil {
return map[string]types.AuthConfig{}
}
return c.context.ConfigFile.AuthConfigs
}

View file

@ -0,0 +1,184 @@
package builder
import (
"fmt"
"io"
"os"
"path"
"path/filepath"
"strings"
"golang.org/x/net/context"
"github.com/Sirupsen/logrus"
"github.com/docker/docker/builder"
"github.com/docker/docker/builder/dockerignore"
"github.com/docker/docker/pkg/archive"
"github.com/docker/docker/pkg/fileutils"
"github.com/docker/docker/pkg/jsonmessage"
"github.com/docker/docker/pkg/progress"
"github.com/docker/docker/pkg/streamformatter"
"github.com/docker/docker/pkg/term"
"github.com/docker/engine-api/client"
"github.com/docker/engine-api/types"
)
// DefaultDockerfileName is the default name of a Dockerfile
const DefaultDockerfileName = "Dockerfile"
// Builder defines methods to provide a docker builder. This makes libcompose
// not tied up to the docker daemon builder.
type Builder interface {
Build(imageName string) error
}
// DaemonBuilder is the daemon "docker build" Builder implementation.
type DaemonBuilder struct {
Client client.ImageAPIClient
ContextDirectory string
Dockerfile string
AuthConfigs map[string]types.AuthConfig
NoCache bool
ForceRemove bool
Pull bool
BuildArgs map[string]string
}
// Build implements Builder. It consumes the docker build API endpoint and sends
// a tar of the specified service build context.
func (d *DaemonBuilder) Build(ctx context.Context, imageName string) error {
buildCtx, err := createTar(d.ContextDirectory, d.Dockerfile)
if err != nil {
return err
}
defer buildCtx.Close()
var progBuff io.Writer = os.Stdout
var buildBuff io.Writer = os.Stdout
// Setup an upload progress bar
progressOutput := streamformatter.NewStreamFormatter().NewProgressOutput(progBuff, true)
var body io.Reader = progress.NewProgressReader(buildCtx, progressOutput, 0, "", "Sending build context to Docker daemon")
logrus.Infof("Building %s...", imageName)
outFd, isTerminalOut := term.GetFdInfo(os.Stdout)
response, err := d.Client.ImageBuild(ctx, body, types.ImageBuildOptions{
Tags: []string{imageName},
NoCache: d.NoCache,
Remove: true,
ForceRemove: d.ForceRemove,
PullParent: d.Pull,
Dockerfile: d.Dockerfile,
AuthConfigs: d.AuthConfigs,
BuildArgs: d.BuildArgs,
})
if err != nil {
return err
}
err = jsonmessage.DisplayJSONMessagesStream(response.Body, buildBuff, outFd, isTerminalOut, nil)
if err != nil {
if jerr, ok := err.(*jsonmessage.JSONError); ok {
// If no error code is set, default to 1
if jerr.Code == 0 {
jerr.Code = 1
}
return fmt.Errorf("Status: %s, Code: %d", jerr.Message, jerr.Code)
}
}
return err
}
// CreateTar create a build context tar for the specified project and service name.
func createTar(contextDirectory, dockerfile string) (io.ReadCloser, error) {
// This code was ripped off from docker/api/client/build.go
dockerfileName := filepath.Join(contextDirectory, dockerfile)
absContextDirectory, err := filepath.Abs(contextDirectory)
if err != nil {
return nil, err
}
filename := dockerfileName
if dockerfile == "" {
// No -f/--file was specified so use the default
dockerfileName = DefaultDockerfileName
filename = filepath.Join(absContextDirectory, dockerfileName)
// Just to be nice ;-) look for 'dockerfile' too but only
// use it if we found it, otherwise ignore this check
if _, err = os.Lstat(filename); os.IsNotExist(err) {
tmpFN := path.Join(absContextDirectory, strings.ToLower(dockerfileName))
if _, err = os.Lstat(tmpFN); err == nil {
dockerfileName = strings.ToLower(dockerfileName)
filename = tmpFN
}
}
}
origDockerfile := dockerfileName // used for error msg
if filename, err = filepath.Abs(filename); err != nil {
return nil, err
}
// Now reset the dockerfileName to be relative to the build context
dockerfileName, err = filepath.Rel(absContextDirectory, filename)
if err != nil {
return nil, err
}
// And canonicalize dockerfile name to a platform-independent one
dockerfileName, err = archive.CanonicalTarNameForPath(dockerfileName)
if err != nil {
return nil, fmt.Errorf("Cannot canonicalize dockerfile path %s: %v", dockerfileName, err)
}
if _, err = os.Lstat(filename); os.IsNotExist(err) {
return nil, fmt.Errorf("Cannot locate Dockerfile: %s", origDockerfile)
}
var includes = []string{"."}
var excludes []string
dockerIgnorePath := path.Join(contextDirectory, ".dockerignore")
dockerIgnore, err := os.Open(dockerIgnorePath)
if err != nil {
if !os.IsNotExist(err) {
return nil, err
}
logrus.Warnf("Error while reading .dockerignore (%s) : %s", dockerIgnorePath, err.Error())
excludes = make([]string, 0)
} else {
excludes, err = dockerignore.ReadAll(dockerIgnore)
if err != nil {
return nil, err
}
}
// If .dockerignore mentions .dockerignore or the Dockerfile
// then make sure we send both files over to the daemon
// because Dockerfile is, obviously, needed no matter what, and
// .dockerignore is needed to know if either one needs to be
// removed. The deamon will remove them for us, if needed, after it
// parses the Dockerfile.
keepThem1, _ := fileutils.Matches(".dockerignore", excludes)
keepThem2, _ := fileutils.Matches(dockerfileName, excludes)
if keepThem1 || keepThem2 {
includes = append(includes, ".dockerignore", dockerfileName)
}
if err := builder.ValidateContextDirectory(contextDirectory, excludes); err != nil {
return nil, fmt.Errorf("Error checking context is accessible: '%s'. Please check permissions and try again.", err)
}
options := &archive.TarOptions{
Compression: archive.Uncompressed,
ExcludePatterns: excludes,
IncludeFiles: includes,
}
return archive.TarWithOptions(contextDirectory, options)
}

View file

@ -0,0 +1,115 @@
package client
import (
"fmt"
"net/http"
"os"
"path/filepath"
"runtime"
"github.com/docker/docker/cliconfig"
"github.com/docker/docker/pkg/homedir"
"github.com/docker/engine-api/client"
"github.com/docker/go-connections/sockets"
"github.com/docker/go-connections/tlsconfig"
"github.com/docker/libcompose/version"
)
const (
// DefaultAPIVersion is the default docker API version set by libcompose
DefaultAPIVersion = "v1.20"
defaultTrustKeyFile = "key.json"
defaultCaFile = "ca.pem"
defaultKeyFile = "key.pem"
defaultCertFile = "cert.pem"
)
var (
dockerCertPath = os.Getenv("DOCKER_CERT_PATH")
)
func init() {
if dockerCertPath == "" {
dockerCertPath = cliconfig.ConfigDir()
}
}
// Options holds docker client options (host, tls, ..)
type Options struct {
TLS bool
TLSVerify bool
TLSOptions tlsconfig.Options
TrustKey string
Host string
APIVersion string
}
// Create creates a docker client based on the specified options.
func Create(c Options) (client.APIClient, error) {
if c.Host == "" {
if os.Getenv("DOCKER_API_VERSION") == "" {
os.Setenv("DOCKER_API_VERSION", DefaultAPIVersion)
}
client, err := client.NewEnvClient()
if err != nil {
return nil, err
}
return client, nil
}
apiVersion := c.APIVersion
if apiVersion == "" {
apiVersion = DefaultAPIVersion
}
if c.TLSOptions.CAFile == "" {
c.TLSOptions.CAFile = filepath.Join(dockerCertPath, defaultCaFile)
}
if c.TLSOptions.CertFile == "" {
c.TLSOptions.CertFile = filepath.Join(dockerCertPath, defaultCertFile)
}
if c.TLSOptions.KeyFile == "" {
c.TLSOptions.KeyFile = filepath.Join(dockerCertPath, defaultKeyFile)
}
if c.TrustKey == "" {
c.TrustKey = filepath.Join(homedir.Get(), ".docker", defaultTrustKeyFile)
}
if c.TLSVerify {
c.TLS = true
}
if c.TLS {
c.TLSOptions.InsecureSkipVerify = !c.TLSVerify
}
var httpClient *http.Client
if c.TLS {
config, err := tlsconfig.Client(c.TLSOptions)
if err != nil {
return nil, err
}
tr := &http.Transport{
TLSClientConfig: config,
}
proto, addr, _, err := client.ParseHost(c.Host)
if err != nil {
return nil, err
}
if err := sockets.ConfigureTransport(tr, proto, addr); err != nil {
return nil, err
}
httpClient = &http.Client{
Transport: tr,
}
}
customHeaders := map[string]string{}
customHeaders["User-Agent"] = fmt.Sprintf("Libcompose-Client/%s (%s)", version.VERSION, runtime.GOOS)
client, err := client.NewClient(c.Host, apiVersion, httpClient, customHeaders)
if err != nil {
return nil, err
}
return client, nil
}

View file

@ -0,0 +1,35 @@
package client
import (
"github.com/docker/engine-api/client"
"github.com/docker/libcompose/project"
)
// Factory is a factory to create docker clients.
type Factory interface {
// Create constructs a Docker client for the given service. The passed in
// config may be nil in which case a generic client for the project should
// be returned.
Create(service project.Service) client.APIClient
}
type defaultFactory struct {
client client.APIClient
}
// NewDefaultFactory creates and returns the default client factory that uses
// github.com/docker/engine-api client.
func NewDefaultFactory(opts Options) (Factory, error) {
client, err := Create(opts)
if err != nil {
return nil, err
}
return &defaultFactory{
client: client,
}, nil
}
func (s *defaultFactory) Create(service project.Service) client.APIClient {
return s.client
}

View file

@ -0,0 +1,766 @@
package docker
import (
"fmt"
"io"
"math"
"os"
"strings"
"time"
"golang.org/x/net/context"
"github.com/Sirupsen/logrus"
"github.com/docker/docker/pkg/promise"
"github.com/docker/docker/pkg/stdcopy"
"github.com/docker/docker/pkg/term"
"github.com/docker/engine-api/client"
"github.com/docker/engine-api/types"
"github.com/docker/engine-api/types/container"
"github.com/docker/engine-api/types/network"
"github.com/docker/go-connections/nat"
"github.com/docker/libcompose/config"
"github.com/docker/libcompose/labels"
"github.com/docker/libcompose/logger"
"github.com/docker/libcompose/project"
"github.com/docker/libcompose/project/events"
util "github.com/docker/libcompose/utils"
"github.com/docker/libcompose/yaml"
)
// Container holds information about a docker container and the service it is tied on.
type Container struct {
name string
serviceName string
projectName string
containerNumber int
oneOff bool
eventNotifier events.Notifier
loggerFactory logger.Factory
client client.APIClient
// FIXME(vdemeester) Remove this dependency
service *Service
}
// NewContainer creates a container struct with the specified docker client, name and service.
func NewContainer(client client.APIClient, name string, containerNumber int, service *Service) *Container {
return &Container{
client: client,
name: name,
containerNumber: containerNumber,
// TODO(vdemeester) Move these to arguments
serviceName: service.name,
projectName: service.project.Name,
eventNotifier: service.project,
loggerFactory: service.context.LoggerFactory,
// TODO(vdemeester) Remove this dependency
service: service,
}
}
// NewOneOffContainer creates a "oneoff" container struct with the specified docker client, name and service.
func NewOneOffContainer(client client.APIClient, name string, containerNumber int, service *Service) *Container {
c := NewContainer(client, name, containerNumber, service)
c.oneOff = true
return c
}
func (c *Container) findExisting(ctx context.Context) (*types.ContainerJSON, error) {
return GetContainer(ctx, c.client, c.name)
}
// Info returns info about the container, like name, command, state or ports.
func (c *Container) Info(ctx context.Context, qFlag bool) (project.Info, error) {
container, err := c.findExisting(ctx)
if err != nil || container == nil {
return nil, err
}
infos, err := GetContainersByFilter(ctx, c.client, map[string][]string{
"name": {container.Name},
})
if err != nil || len(infos) == 0 {
return nil, err
}
info := infos[0]
result := project.Info{}
if qFlag {
result = append(result, project.InfoPart{Key: "Id", Value: container.ID})
} else {
result = append(result, project.InfoPart{Key: "Name", Value: name(info.Names)})
result = append(result, project.InfoPart{Key: "Command", Value: info.Command})
result = append(result, project.InfoPart{Key: "State", Value: info.Status})
result = append(result, project.InfoPart{Key: "Ports", Value: portString(info.Ports)})
}
return result, nil
}
func portString(ports []types.Port) string {
result := []string{}
for _, port := range ports {
if port.PublicPort > 0 {
result = append(result, fmt.Sprintf("%s:%d->%d/%s", port.IP, port.PublicPort, port.PrivatePort, port.Type))
} else {
result = append(result, fmt.Sprintf("%d/%s", port.PrivatePort, port.Type))
}
}
return strings.Join(result, ", ")
}
func name(names []string) string {
max := math.MaxInt32
var current string
for _, v := range names {
if len(v) < max {
max = len(v)
current = v
}
}
return current[1:]
}
// Recreate will not refresh the container by means of relaxation and enjoyment,
// just delete it and create a new one with the current configuration
func (c *Container) Recreate(ctx context.Context, imageName string) (*types.ContainerJSON, error) {
container, err := c.findExisting(ctx)
if err != nil || container == nil {
return nil, err
}
hash := container.Config.Labels[labels.HASH.Str()]
if hash == "" {
return nil, fmt.Errorf("Failed to find hash on old container: %s", container.Name)
}
name := container.Name[1:]
newName := fmt.Sprintf("%s_%s", name, container.ID[:12])
logrus.Debugf("Renaming %s => %s", name, newName)
if err := c.client.ContainerRename(ctx, container.ID, newName); err != nil {
logrus.Errorf("Failed to rename old container %s", c.name)
return nil, err
}
newContainer, err := c.createContainer(ctx, imageName, container.ID, nil)
if err != nil {
return nil, err
}
logrus.Debugf("Created replacement container %s", newContainer.ID)
if err := c.client.ContainerRemove(ctx, container.ID, types.ContainerRemoveOptions{
Force: true,
RemoveVolumes: false,
}); err != nil {
logrus.Errorf("Failed to remove old container %s", c.name)
return nil, err
}
logrus.Debugf("Removed old container %s %s", c.name, container.ID)
return newContainer, nil
}
// Create creates the container based on the specified image name and send an event
// to notify the container has been created. If the container already exists, does
// nothing.
func (c *Container) Create(ctx context.Context, imageName string) (*types.ContainerJSON, error) {
return c.CreateWithOverride(ctx, imageName, nil)
}
// CreateWithOverride create container and override parts of the config to
// allow special situations to override the config generated from the compose
// file
func (c *Container) CreateWithOverride(ctx context.Context, imageName string, configOverride *config.ServiceConfig) (*types.ContainerJSON, error) {
container, err := c.findExisting(ctx)
if err != nil {
return nil, err
}
if container == nil {
container, err = c.createContainer(ctx, imageName, "", configOverride)
if err != nil {
return nil, err
}
c.eventNotifier.Notify(events.ContainerCreated, c.serviceName, map[string]string{
"name": c.Name(),
})
}
return container, err
}
// Stop stops the container.
func (c *Container) Stop(ctx context.Context, timeout int) error {
return c.withContainer(ctx, func(container *types.ContainerJSON) error {
timeoutDuration := time.Duration(timeout) * time.Second
return c.client.ContainerStop(ctx, container.ID, &timeoutDuration)
})
}
// Pause pauses the container. If the containers are already paused, don't fail.
func (c *Container) Pause(ctx context.Context) error {
return c.withContainer(ctx, func(container *types.ContainerJSON) error {
if !container.State.Paused {
return c.client.ContainerPause(ctx, container.ID)
}
return nil
})
}
// Unpause unpauses the container. If the containers are not paused, don't fail.
func (c *Container) Unpause(ctx context.Context) error {
return c.withContainer(ctx, func(container *types.ContainerJSON) error {
if container.State.Paused {
return c.client.ContainerUnpause(ctx, container.ID)
}
return nil
})
}
// Kill kill the container.
func (c *Container) Kill(ctx context.Context, signal string) error {
return c.withContainer(ctx, func(container *types.ContainerJSON) error {
return c.client.ContainerKill(ctx, container.ID, signal)
})
}
// Delete removes the container if existing. If the container is running, it tries
// to stop it first.
func (c *Container) Delete(ctx context.Context, removeVolume bool) error {
container, err := c.findExisting(ctx)
if err != nil || container == nil {
return err
}
info, err := c.client.ContainerInspect(ctx, container.ID)
if err != nil {
return err
}
if !info.State.Running {
return c.client.ContainerRemove(ctx, container.ID, types.ContainerRemoveOptions{
Force: true,
RemoveVolumes: removeVolume,
})
}
return nil
}
// IsRunning returns the running state of the container.
func (c *Container) IsRunning(ctx context.Context) (bool, error) {
container, err := c.findExisting(ctx)
if err != nil || container == nil {
return false, err
}
info, err := c.client.ContainerInspect(ctx, container.ID)
if err != nil {
return false, err
}
return info.State.Running, nil
}
// Run creates, start and attach to the container based on the image name,
// the specified configuration.
// It will always create a new container.
func (c *Container) Run(ctx context.Context, configOverride *config.ServiceConfig) (int, error) {
var (
errCh chan error
out, stderr io.Writer
in io.ReadCloser
)
container, err := c.findExisting(ctx)
if err != nil || container == nil {
return -1, err
}
if configOverride.StdinOpen {
in = os.Stdin
}
if configOverride.Tty {
out = os.Stdout
}
if configOverride.Tty {
stderr = os.Stderr
}
options := types.ContainerAttachOptions{
Stream: true,
Stdin: configOverride.StdinOpen,
Stdout: configOverride.Tty,
Stderr: configOverride.Tty,
}
resp, err := c.client.ContainerAttach(ctx, container.ID, options)
if err != nil {
return -1, err
}
// set raw terminal
inFd, _ := term.GetFdInfo(in)
state, err := term.SetRawTerminal(inFd)
if err != nil {
return -1, err
}
// restore raw terminal
defer term.RestoreTerminal(inFd, state)
// holdHijackedConnection (in goroutine)
errCh = promise.Go(func() error {
return holdHijackedConnection(configOverride.Tty, in, out, stderr, resp)
})
if err := c.client.ContainerStart(ctx, container.ID, types.ContainerStartOptions{}); err != nil {
return -1, err
}
if err := <-errCh; err != nil {
logrus.Debugf("Error hijack: %s", err)
return -1, err
}
exitedContainer, err := c.client.ContainerInspect(ctx, container.ID)
if err != nil {
return -1, err
}
return exitedContainer.State.ExitCode, nil
}
func holdHijackedConnection(tty bool, inputStream io.ReadCloser, outputStream, errorStream io.Writer, resp types.HijackedResponse) error {
var err error
receiveStdout := make(chan error, 1)
if outputStream != nil || errorStream != nil {
go func() {
// When TTY is ON, use regular copy
if tty && outputStream != nil {
_, err = io.Copy(outputStream, resp.Reader)
} else {
_, err = stdcopy.StdCopy(outputStream, errorStream, resp.Reader)
}
logrus.Debugf("[hijack] End of stdout")
receiveStdout <- err
}()
}
stdinDone := make(chan struct{})
go func() {
if inputStream != nil {
io.Copy(resp.Conn, inputStream)
logrus.Debugf("[hijack] End of stdin")
}
if err := resp.CloseWrite(); err != nil {
logrus.Debugf("Couldn't send EOF: %s", err)
}
close(stdinDone)
}()
select {
case err := <-receiveStdout:
if err != nil {
logrus.Debugf("Error receiveStdout: %s", err)
return err
}
case <-stdinDone:
if outputStream != nil || errorStream != nil {
if err := <-receiveStdout; err != nil {
logrus.Debugf("Error receiveStdout: %s", err)
return err
}
}
}
return nil
}
// Start the specified container with the specified host config
func (c *Container) Start(ctx context.Context) error {
container, err := c.findExisting(ctx)
if err != nil || container == nil {
return err
}
logrus.WithFields(logrus.Fields{"container.ID": container.ID, "c.name": c.name}).Debug("Starting container")
if err := c.client.ContainerStart(context.Background(), container.ID, types.ContainerStartOptions{}); err != nil {
logrus.WithFields(logrus.Fields{"container.ID": container.ID, "c.name": c.name}).Debug("Failed to start container")
return err
}
c.eventNotifier.Notify(events.ContainerStarted, c.serviceName, map[string]string{
"name": c.Name(),
})
return nil
}
// OutOfSync checks if the container is out of sync with the service definition.
// It looks if the the service hash container label is the same as the computed one.
func (c *Container) OutOfSync(ctx context.Context, imageName string) (bool, error) {
container, err := c.findExisting(ctx)
if err != nil || container == nil {
return false, err
}
if container.Config.Image != imageName {
logrus.Debugf("Images for %s do not match %s!=%s", c.name, container.Config.Image, imageName)
return true, nil
}
if container.Config.Labels[labels.HASH.Str()] != c.getHash() {
logrus.Debugf("Hashes for %s do not match %s!=%s", c.name, container.Config.Labels[labels.HASH.Str()], c.getHash())
return true, nil
}
image, _, err := c.client.ImageInspectWithRaw(ctx, container.Config.Image, false)
if err != nil {
if client.IsErrImageNotFound(err) {
logrus.Debugf("Image %s do not exist, do not know if it's out of sync", container.Config.Image)
return false, nil
}
return false, err
}
logrus.Debugf("Checking existing image name vs id: %s == %s", image.ID, container.Image)
return image.ID != container.Image, err
}
func (c *Container) getHash() string {
return config.GetServiceHash(c.serviceName, c.service.Config())
}
func volumeBinds(volumes map[string]struct{}, container *types.ContainerJSON) []string {
result := make([]string, 0, len(container.Mounts))
for _, mount := range container.Mounts {
if _, ok := volumes[mount.Destination]; ok {
result = append(result, fmt.Sprint(mount.Source, ":", mount.Destination))
}
}
return result
}
func (c *Container) createContainer(ctx context.Context, imageName, oldContainer string, configOverride *config.ServiceConfig) (*types.ContainerJSON, error) {
serviceConfig := c.service.serviceConfig
if configOverride != nil {
serviceConfig.Command = configOverride.Command
serviceConfig.Tty = configOverride.Tty
serviceConfig.StdinOpen = configOverride.StdinOpen
}
configWrapper, err := ConvertToAPI(c.service)
if err != nil {
return nil, err
}
configWrapper.Config.Image = imageName
if configWrapper.Config.Labels == nil {
configWrapper.Config.Labels = map[string]string{}
}
oneOffString := "False"
if c.oneOff {
oneOffString = "True"
}
configWrapper.Config.Labels[labels.SERVICE.Str()] = c.serviceName
configWrapper.Config.Labels[labels.PROJECT.Str()] = c.projectName
configWrapper.Config.Labels[labels.HASH.Str()] = c.getHash()
configWrapper.Config.Labels[labels.ONEOFF.Str()] = oneOffString
configWrapper.Config.Labels[labels.NUMBER.Str()] = fmt.Sprint(c.containerNumber)
configWrapper.Config.Labels[labels.VERSION.Str()] = ComposeVersion
err = c.populateAdditionalHostConfig(configWrapper.HostConfig)
if err != nil {
return nil, err
}
if oldContainer != "" {
info, err := c.client.ContainerInspect(ctx, oldContainer)
if err != nil {
return nil, err
}
configWrapper.HostConfig.Binds = util.Merge(configWrapper.HostConfig.Binds, volumeBinds(configWrapper.Config.Volumes, &info))
}
logrus.Debugf("Creating container %s %#v", c.name, configWrapper)
container, err := c.client.ContainerCreate(ctx, configWrapper.Config, configWrapper.HostConfig, configWrapper.NetworkingConfig, c.name)
if err != nil {
logrus.Debugf("Failed to create container %s: %v", c.name, err)
return nil, err
}
return GetContainer(ctx, c.client, container.ID)
}
func (c *Container) populateAdditionalHostConfig(hostConfig *container.HostConfig) error {
links, err := c.getLinks()
if err != nil {
return err
}
for _, link := range c.service.DependentServices() {
if !c.service.project.ServiceConfigs.Has(link.Target) {
continue
}
service, err := c.service.project.CreateService(link.Target)
if err != nil {
return err
}
// FIXME(vdemeester) container should not know service
containers, err := service.Containers(context.Background())
if err != nil {
return err
}
if link.Type == project.RelTypeIpcNamespace {
hostConfig, err = c.addIpc(hostConfig, service, containers)
} else if link.Type == project.RelTypeNetNamespace {
hostConfig, err = c.addNetNs(hostConfig, service, containers)
}
if err != nil {
return err
}
}
hostConfig.Links = []string{}
for k, v := range links {
hostConfig.Links = append(hostConfig.Links, strings.Join([]string{v, k}, ":"))
}
for _, v := range c.service.Config().ExternalLinks {
hostConfig.Links = append(hostConfig.Links, v)
}
return nil
}
// FIXME(vdemeester) this is temporary
func (c *Container) getLinks() (map[string]string, error) {
links := map[string]string{}
for _, link := range c.service.DependentServices() {
if !c.service.project.ServiceConfigs.Has(link.Target) {
continue
}
service, err := c.service.project.CreateService(link.Target)
if err != nil {
return nil, err
}
// FIXME(vdemeester) container should not know service
containers, err := service.Containers(context.Background())
if err != nil {
return nil, err
}
if link.Type == project.RelTypeLink {
c.addLinks(links, service, link, containers)
}
if err != nil {
return nil, err
}
}
return links, nil
}
func (c *Container) addLinks(links map[string]string, service project.Service, rel project.ServiceRelationship, containers []project.Container) {
for _, container := range containers {
if _, ok := links[rel.Alias]; !ok {
links[rel.Alias] = container.Name()
}
links[container.Name()] = container.Name()
}
}
func (c *Container) addIpc(config *container.HostConfig, service project.Service, containers []project.Container) (*container.HostConfig, error) {
if len(containers) == 0 {
return nil, fmt.Errorf("Failed to find container for IPC %v", c.service.Config().Ipc)
}
id, err := containers[0].ID()
if err != nil {
return nil, err
}
config.IpcMode = container.IpcMode("container:" + id)
return config, nil
}
func (c *Container) addNetNs(config *container.HostConfig, service project.Service, containers []project.Container) (*container.HostConfig, error) {
if len(containers) == 0 {
return nil, fmt.Errorf("Failed to find container for networks ns %v", c.service.Config().NetworkMode)
}
id, err := containers[0].ID()
if err != nil {
return nil, err
}
config.NetworkMode = container.NetworkMode("container:" + id)
return config, nil
}
// ID returns the container Id.
func (c *Container) ID() (string, error) {
// FIXME(vdemeester) container should not ask for his ID..
container, err := c.findExisting(context.Background())
if container == nil {
return "", err
}
return container.ID, err
}
// Name returns the container name.
func (c *Container) Name() string {
return c.name
}
// Restart restarts the container if existing, does nothing otherwise.
func (c *Container) Restart(ctx context.Context, timeout int) error {
container, err := c.findExisting(ctx)
if err != nil || container == nil {
return err
}
timeoutDuration := time.Duration(timeout) * time.Second
return c.client.ContainerRestart(ctx, container.ID, &timeoutDuration)
}
// Log forwards container logs to the project configured logger.
func (c *Container) Log(ctx context.Context, follow bool) error {
container, err := c.findExisting(ctx)
if container == nil || err != nil {
return err
}
info, err := c.client.ContainerInspect(ctx, container.ID)
if err != nil {
return err
}
// FIXME(vdemeester) update container struct to do less API calls
name := fmt.Sprintf("%s_%d", c.service.name, c.containerNumber)
l := c.loggerFactory.Create(name)
options := types.ContainerLogsOptions{
ShowStdout: true,
ShowStderr: true,
Follow: follow,
Tail: "all",
}
responseBody, err := c.client.ContainerLogs(ctx, c.name, options)
if err != nil {
return err
}
defer responseBody.Close()
if info.Config.Tty {
_, err = io.Copy(&logger.Wrapper{Logger: l}, responseBody)
} else {
_, err = stdcopy.StdCopy(&logger.Wrapper{Logger: l}, &logger.Wrapper{Logger: l, Err: true}, responseBody)
}
logrus.WithFields(logrus.Fields{"Logger": l, "err": err}).Debug("c.client.Logs() returned error")
return err
}
func (c *Container) withContainer(ctx context.Context, action func(*types.ContainerJSON) error) error {
container, err := c.findExisting(ctx)
if err != nil {
return err
}
if container != nil {
return action(container)
}
return nil
}
// Port returns the host port the specified port is mapped on.
func (c *Container) Port(ctx context.Context, port string) (string, error) {
container, err := c.findExisting(ctx)
if err != nil {
return "", err
}
if bindings, ok := container.NetworkSettings.Ports[nat.Port(port)]; ok {
result := []string{}
for _, binding := range bindings {
result = append(result, binding.HostIP+":"+binding.HostPort)
}
return strings.Join(result, "\n"), nil
}
return "", nil
}
// Networks returns the containers network
// FIXME(vdemeester) should not need ctx or calling the API, will take care of it
// when refactoring Container.
func (c *Container) Networks(ctx context.Context) (map[string]*network.EndpointSettings, error) {
container, err := c.findExisting(ctx)
if err != nil {
return nil, err
}
if container == nil {
return map[string]*network.EndpointSettings{}, nil
}
return container.NetworkSettings.Networks, nil
}
// NetworkDisconnect disconnects the container from the specified network
// FIXME(vdemeester) will be refactor with Container refactoring
func (c *Container) NetworkDisconnect(ctx context.Context, net *yaml.Network) error {
container, err := c.findExisting(ctx)
if err != nil || container == nil {
return err
}
return c.client.NetworkDisconnect(ctx, net.RealName, container.ID, true)
}
// NetworkConnect connects the container to the specified network
// FIXME(vdemeester) will be refactor with Container refactoring
func (c *Container) NetworkConnect(ctx context.Context, net *yaml.Network) error {
container, err := c.findExisting(ctx)
if err != nil || container == nil {
return err
}
internalLinks, err := c.getLinks()
if err != nil {
return err
}
links := []string{}
// TODO(vdemeester) handle link to self (?)
for k, v := range internalLinks {
links = append(links, strings.Join([]string{v, k}, ":"))
}
for _, v := range c.service.Config().ExternalLinks {
links = append(links, v)
}
aliases := []string{}
if !c.oneOff {
aliases = []string{c.serviceName}
}
aliases = append(aliases, net.Aliases...)
return c.client.NetworkConnect(ctx, net.RealName, container.ID, &network.EndpointSettings{
Aliases: aliases,
Links: links,
IPAddress: net.IPv4Address,
IPAMConfig: &network.EndpointIPAMConfig{
IPv4Address: net.IPv4Address,
IPv6Address: net.IPv6Address,
},
})
}

View file

@ -0,0 +1,38 @@
package docker
import (
"github.com/docker/docker/cliconfig"
"github.com/docker/docker/cliconfig/configfile"
"github.com/docker/libcompose/docker/client"
"github.com/docker/libcompose/project"
)
// Context holds context meta information about a libcompose project and docker
// client information (like configuration file, builder to use, …)
type Context struct {
project.Context
ClientFactory client.Factory
ConfigDir string
ConfigFile *configfile.ConfigFile
AuthLookup AuthLookup
}
func (c *Context) open() error {
return c.LookupConfig()
}
// LookupConfig tries to load the docker configuration files, if any.
func (c *Context) LookupConfig() error {
if c.ConfigFile != nil {
return nil
}
config, err := cliconfig.Load(c.ConfigDir)
if err != nil {
return err
}
c.ConfigFile = config
return nil
}

View file

@ -0,0 +1,288 @@
package docker
import (
"fmt"
"strings"
"golang.org/x/net/context"
"github.com/docker/docker/runconfig/opts"
"github.com/docker/engine-api/types/container"
"github.com/docker/engine-api/types/network"
"github.com/docker/engine-api/types/strslice"
"github.com/docker/go-connections/nat"
"github.com/docker/go-units"
"github.com/docker/libcompose/config"
composeclient "github.com/docker/libcompose/docker/client"
"github.com/docker/libcompose/project"
"github.com/docker/libcompose/utils"
)
// ConfigWrapper wraps Config, HostConfig and NetworkingConfig for a container.
type ConfigWrapper struct {
Config *container.Config
HostConfig *container.HostConfig
NetworkingConfig *network.NetworkingConfig
}
// Filter filters the specified string slice with the specified function.
func Filter(vs []string, f func(string) bool) []string {
r := make([]string, 0, len(vs))
for _, v := range vs {
if f(v) {
r = append(r, v)
}
}
return r
}
func isBind(s string) bool {
return strings.ContainsRune(s, ':')
}
func isVolume(s string) bool {
return !isBind(s)
}
// ConvertToAPI converts a service configuration to a docker API container configuration.
func ConvertToAPI(s *Service) (*ConfigWrapper, error) {
config, hostConfig, err := Convert(s.serviceConfig, s.context.Context, s.clientFactory)
if err != nil {
return nil, err
}
result := ConfigWrapper{
Config: config,
HostConfig: hostConfig,
}
return &result, nil
}
func isNamedVolume(volume string) bool {
return !strings.HasPrefix(volume, ".") && !strings.HasPrefix(volume, "/") && !strings.HasPrefix(volume, "~")
}
func volumes(c *config.ServiceConfig, ctx project.Context) map[string]struct{} {
volumes := make(map[string]struct{}, len(c.Volumes))
for k, v := range c.Volumes {
if len(ctx.ComposeFiles) > 0 && !isNamedVolume(v) {
v = ctx.ResourceLookup.ResolvePath(v, ctx.ComposeFiles[0])
}
c.Volumes[k] = v
if isVolume(v) {
volumes[v] = struct{}{}
}
}
return volumes
}
func restartPolicy(c *config.ServiceConfig) (*container.RestartPolicy, error) {
restart, err := opts.ParseRestartPolicy(c.Restart)
if err != nil {
return nil, err
}
return &container.RestartPolicy{Name: restart.Name, MaximumRetryCount: restart.MaximumRetryCount}, nil
}
func ports(c *config.ServiceConfig) (map[nat.Port]struct{}, nat.PortMap, error) {
ports, binding, err := nat.ParsePortSpecs(c.Ports)
if err != nil {
return nil, nil, err
}
exPorts, _, err := nat.ParsePortSpecs(c.Expose)
if err != nil {
return nil, nil, err
}
for k, v := range exPorts {
ports[k] = v
}
exposedPorts := map[nat.Port]struct{}{}
for k, v := range ports {
exposedPorts[nat.Port(k)] = v
}
portBindings := nat.PortMap{}
for k, bv := range binding {
dcbs := make([]nat.PortBinding, len(bv))
for k, v := range bv {
dcbs[k] = nat.PortBinding{HostIP: v.HostIP, HostPort: v.HostPort}
}
portBindings[nat.Port(k)] = dcbs
}
return exposedPorts, portBindings, nil
}
// Convert converts a service configuration to an docker API structures (Config and HostConfig)
func Convert(c *config.ServiceConfig, ctx project.Context, clientFactory composeclient.Factory) (*container.Config, *container.HostConfig, error) {
restartPolicy, err := restartPolicy(c)
if err != nil {
return nil, nil, err
}
exposedPorts, portBindings, err := ports(c)
if err != nil {
return nil, nil, err
}
deviceMappings, err := parseDevices(c.Devices)
if err != nil {
return nil, nil, err
}
var volumesFrom []string
if c.VolumesFrom != nil {
volumesFrom, err = getVolumesFrom(c.VolumesFrom, ctx.Project.ServiceConfigs, ctx.ProjectName)
if err != nil {
return nil, nil, err
}
}
config := &container.Config{
Entrypoint: strslice.StrSlice(utils.CopySlice(c.Entrypoint)),
Hostname: c.Hostname,
Domainname: c.DomainName,
User: c.User,
Env: utils.CopySlice(c.Environment),
Cmd: strslice.StrSlice(utils.CopySlice(c.Command)),
Image: c.Image,
Labels: utils.CopyMap(c.Labels),
ExposedPorts: exposedPorts,
Tty: c.Tty,
OpenStdin: c.StdinOpen,
WorkingDir: c.WorkingDir,
Volumes: volumes(c, ctx),
MacAddress: c.MacAddress,
}
ulimits := []*units.Ulimit{}
if c.Ulimits.Elements != nil {
for _, ulimit := range c.Ulimits.Elements {
ulimits = append(ulimits, &units.Ulimit{
Name: ulimit.Name,
Soft: ulimit.Soft,
Hard: ulimit.Hard,
})
}
}
resources := container.Resources{
CgroupParent: c.CgroupParent,
Memory: c.MemLimit,
MemorySwap: c.MemSwapLimit,
CPUShares: c.CPUShares,
CPUQuota: c.CPUQuota,
CpusetCpus: c.CPUSet,
Ulimits: ulimits,
Devices: deviceMappings,
}
networkMode := c.NetworkMode
if c.NetworkMode == "" {
if c.Networks != nil && len(c.Networks.Networks) > 0 {
networkMode = c.Networks.Networks[0].RealName
}
} else {
switch {
case strings.HasPrefix(c.NetworkMode, "service:"):
serviceName := c.NetworkMode[8:]
if serviceConfig, ok := ctx.Project.ServiceConfigs.Get(serviceName); ok {
// FIXME(vdemeester) this is actually not right, should be fixed but not there
service, err := ctx.ServiceFactory.Create(ctx.Project, serviceName, serviceConfig)
if err != nil {
return nil, nil, err
}
containers, err := service.Containers(context.Background())
if err != nil {
return nil, nil, err
}
if len(containers) != 0 {
container := containers[0]
containerID, err := container.ID()
if err != nil {
return nil, nil, err
}
networkMode = "container:" + containerID
}
// FIXME(vdemeester) log/warn in case of len(containers) == 0
}
case strings.HasPrefix(c.NetworkMode, "container:"):
containerName := c.NetworkMode[10:]
client := clientFactory.Create(nil)
container, err := GetContainer(context.Background(), client, containerName)
if err != nil {
return nil, nil, err
}
networkMode = "container:" + container.ID
default:
// do nothing :)
}
}
hostConfig := &container.HostConfig{
VolumesFrom: volumesFrom,
CapAdd: strslice.StrSlice(utils.CopySlice(c.CapAdd)),
CapDrop: strslice.StrSlice(utils.CopySlice(c.CapDrop)),
ExtraHosts: utils.CopySlice(c.ExtraHosts),
Privileged: c.Privileged,
Binds: Filter(c.Volumes, isBind),
DNS: utils.CopySlice(c.DNS),
DNSSearch: utils.CopySlice(c.DNSSearch),
LogConfig: container.LogConfig{
Type: c.Logging.Driver,
Config: utils.CopyMap(c.Logging.Options),
},
NetworkMode: container.NetworkMode(networkMode),
ReadonlyRootfs: c.ReadOnly,
PidMode: container.PidMode(c.Pid),
UTSMode: container.UTSMode(c.Uts),
IpcMode: container.IpcMode(c.Ipc),
PortBindings: portBindings,
RestartPolicy: *restartPolicy,
ShmSize: c.ShmSize,
SecurityOpt: utils.CopySlice(c.SecurityOpt),
VolumeDriver: c.VolumeDriver,
Resources: resources,
}
return config, hostConfig, nil
}
func getVolumesFrom(volumesFrom []string, serviceConfigs *config.ServiceConfigs, projectName string) ([]string, error) {
volumes := []string{}
for _, volumeFrom := range volumesFrom {
if serviceConfig, ok := serviceConfigs.Get(volumeFrom); ok {
// It's a service - Use the first one
name := fmt.Sprintf("%s_%s_1", projectName, volumeFrom)
// If a container name is specified, use that instead
if serviceConfig.ContainerName != "" {
name = serviceConfig.ContainerName
}
volumes = append(volumes, name)
} else {
volumes = append(volumes, volumeFrom)
}
}
return volumes, nil
}
func parseDevices(devices []string) ([]container.DeviceMapping, error) {
// parse device mappings
deviceMappings := []container.DeviceMapping{}
for _, device := range devices {
v, err := opts.ParseDevice(device)
if err != nil {
return nil, err
}
deviceMappings = append(deviceMappings, container.DeviceMapping{
PathOnHost: v.PathOnHost,
PathInContainer: v.PathInContainer,
CgroupPermissions: v.CgroupPermissions,
})
}
return deviceMappings, nil
}

View file

@ -0,0 +1,42 @@
package docker
import (
"golang.org/x/net/context"
"github.com/docker/engine-api/client"
"github.com/docker/engine-api/types"
"github.com/docker/engine-api/types/filters"
)
// GetContainersByFilter looks up the hosts containers with the specified filters and
// returns a list of container matching it, or an error.
func GetContainersByFilter(ctx context.Context, clientInstance client.APIClient, containerFilters ...map[string][]string) ([]types.Container, error) {
filterArgs := filters.NewArgs()
// FIXME(vdemeester) I don't like 3 for loops >_<
for _, filter := range containerFilters {
for key, filterValue := range filter {
for _, value := range filterValue {
filterArgs.Add(key, value)
}
}
}
return clientInstance.ContainerList(ctx, types.ContainerListOptions{
All: true,
Filter: filterArgs,
})
}
// GetContainer looks up the hosts containers with the specified ID
// or name and returns it, or an error.
func GetContainer(ctx context.Context, clientInstance client.APIClient, id string) (*types.ContainerJSON, error) {
container, err := clientInstance.ContainerInspect(ctx, id)
if err != nil {
if client.IsErrContainerNotFound(err) {
return nil, nil
}
return nil, err
}
return &container, nil
}

View file

@ -0,0 +1,80 @@
package docker
import (
"encoding/base64"
"encoding/json"
"fmt"
"io"
"os"
"golang.org/x/net/context"
"github.com/Sirupsen/logrus"
"github.com/docker/docker/pkg/jsonmessage"
"github.com/docker/docker/pkg/term"
"github.com/docker/docker/reference"
"github.com/docker/docker/registry"
"github.com/docker/engine-api/client"
"github.com/docker/engine-api/types"
)
func removeImage(ctx context.Context, client client.APIClient, image string) error {
_, err := client.ImageRemove(ctx, image, types.ImageRemoveOptions{})
return err
}
func pullImage(ctx context.Context, client client.APIClient, service *Service, image string) error {
fmt.Fprintf(os.Stderr, "Pulling %s (%s)...\n", service.name, image)
distributionRef, err := reference.ParseNamed(image)
if err != nil {
return err
}
repoInfo, err := registry.ParseRepositoryInfo(distributionRef)
if err != nil {
return err
}
authConfig := service.authLookup.Lookup(repoInfo)
encodedAuth, err := encodeAuthToBase64(authConfig)
if err != nil {
return err
}
options := types.ImagePullOptions{
RegistryAuth: encodedAuth,
}
responseBody, err := client.ImagePull(ctx, distributionRef.String(), options)
if err != nil {
logrus.Errorf("Failed to pull image %s: %v", image, err)
return err
}
defer responseBody.Close()
var writeBuff io.Writer = os.Stderr
outFd, isTerminalOut := term.GetFdInfo(os.Stderr)
err = jsonmessage.DisplayJSONMessagesStream(responseBody, writeBuff, outFd, isTerminalOut, nil)
if err != nil {
if jerr, ok := err.(*jsonmessage.JSONError); ok {
// If no error code is set, default to 1
if jerr.Code == 0 {
jerr.Code = 1
}
fmt.Fprintf(os.Stderr, "%s", writeBuff)
return fmt.Errorf("Status: %s, Code: %d", jerr.Message, jerr.Code)
}
}
return err
}
// encodeAuthToBase64 serializes the auth configuration as JSON base64 payload
func encodeAuthToBase64(authConfig types.AuthConfig) (string, error) {
buf, err := json.Marshal(authConfig)
if err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(buf), nil
}

View file

@ -0,0 +1,92 @@
package docker
import (
"fmt"
"strconv"
"golang.org/x/net/context"
"github.com/docker/engine-api/client"
"github.com/docker/engine-api/types"
"github.com/docker/engine-api/types/filters"
"github.com/docker/libcompose/labels"
)
const format = "%s_%s_%d"
// Namer defines method to provide container name.
type Namer interface {
Next() (string, int)
}
type defaultNamer struct {
project string
service string
oneOff bool
currentNumber int
}
type singleNamer struct {
name string
}
// NewSingleNamer returns a namer that only allows a single name.
func NewSingleNamer(name string) Namer {
return &singleNamer{name}
}
// NewNamer returns a namer that returns names based on the specified project and
// service name and an inner counter, e.g. project_service_1, project_service_2…
func NewNamer(ctx context.Context, client client.ContainerAPIClient, project, service string, oneOff bool) (Namer, error) {
namer := &defaultNamer{
project: project,
service: service,
oneOff: oneOff,
}
filter := filters.NewArgs()
filter.Add("label", fmt.Sprintf("%s=%s", labels.PROJECT.Str(), project))
filter.Add("label", fmt.Sprintf("%s=%s", labels.SERVICE.Str(), service))
if oneOff {
filter.Add("label", fmt.Sprintf("%s=%s", labels.ONEOFF.Str(), "True"))
} else {
filter.Add("label", fmt.Sprintf("%s=%s", labels.ONEOFF.Str(), "False"))
}
containers, err := client.ContainerList(ctx, types.ContainerListOptions{
All: true,
Filter: filter,
})
if err != nil {
return nil, err
}
maxNumber := 0
for _, container := range containers {
number, err := strconv.Atoi(container.Labels[labels.NUMBER.Str()])
if err != nil {
return nil, err
}
if number > maxNumber {
maxNumber = number
}
}
namer.currentNumber = maxNumber + 1
return namer, nil
}
func (i *defaultNamer) Next() (string, int) {
service := i.service
if i.oneOff {
service = i.service + "_run"
}
name := fmt.Sprintf(format, i.project, service, i.currentNumber)
number := i.currentNumber
i.currentNumber = i.currentNumber + 1
return name, number
}
func (s *singleNamer) Next() (string, int) {
return s.name, 1
}

View file

@ -0,0 +1,19 @@
package network
import (
"github.com/docker/libcompose/config"
composeclient "github.com/docker/libcompose/docker/client"
"github.com/docker/libcompose/project"
)
// DockerFactory implements project.NetworksFactory
type DockerFactory struct {
ClientFactory composeclient.Factory
}
// Create implements project.NetworksFactory Create method.
// It creates a Networks (that implements project.Networks) from specified configurations.
func (f *DockerFactory) Create(projectName string, networkConfigs map[string]*config.NetworkConfig, serviceConfigs *config.ServiceConfigs, networkEnabled bool) (project.Networks, error) {
cli := f.ClientFactory.Create(nil)
return NetworksFromServices(cli, projectName, networkConfigs, serviceConfigs, networkEnabled)
}

View file

@ -0,0 +1,194 @@
package network
import (
"fmt"
"reflect"
"strings"
"golang.org/x/net/context"
"github.com/docker/engine-api/client"
"github.com/docker/engine-api/types"
"github.com/docker/engine-api/types/network"
"github.com/docker/libcompose/config"
"github.com/docker/libcompose/yaml"
)
// Network holds attributes and method for a network definition in compose
type Network struct {
client client.NetworkAPIClient
name string
projectName string
driver string
driverOptions map[string]string
ipam config.Ipam
external bool
}
func (n *Network) fullName() string {
name := n.projectName + "_" + n.name
if n.external {
name = n.name
}
return name
}
// Inspect inspect the current network
func (n *Network) Inspect(ctx context.Context) (types.NetworkResource, error) {
return n.client.NetworkInspect(ctx, n.fullName())
}
// Remove removes the current network (from docker engine)
func (n *Network) Remove(ctx context.Context) error {
if n.external {
fmt.Printf("Network %s is external, skipping", n.fullName())
return nil
}
fmt.Printf("Removing network %q\n", n.fullName())
return n.client.NetworkRemove(ctx, n.fullName())
}
// EnsureItExists make sure the network exists and return an error if it does not exists
// and cannot be created.
func (n *Network) EnsureItExists(ctx context.Context) error {
networkResource, err := n.Inspect(ctx)
if n.external {
if client.IsErrNetworkNotFound(err) {
// FIXME(vdemeester) introduce some libcompose error type
return fmt.Errorf("Network %s declared as external, but could not be found. Please create the network manually using docker network create %s and try again", n.fullName(), n.fullName())
}
return err
}
if err != nil && client.IsErrNetworkNotFound(err) {
return n.create(ctx)
}
if n.driver != "" && networkResource.Driver != n.driver {
return fmt.Errorf("Network %q needs to be recreated - driver has changed", n.fullName())
}
if len(n.driverOptions) != 0 && !reflect.DeepEqual(networkResource.Options, n.driverOptions) {
return fmt.Errorf("Network %q needs to be recreated - options have changed", n.fullName())
}
return err
}
func (n *Network) create(ctx context.Context) error {
fmt.Printf("Creating network %q with driver %q\n", n.fullName(), n.driver)
_, err := n.client.NetworkCreate(ctx, n.fullName(), types.NetworkCreate{
Driver: n.driver,
Options: n.driverOptions,
IPAM: convertToAPIIpam(n.ipam),
})
return err
}
func convertToAPIIpam(ipam config.Ipam) network.IPAM {
ipamConfigs := []network.IPAMConfig{}
for _, config := range ipam.Config {
ipamConfigs = append(ipamConfigs, network.IPAMConfig{
Subnet: config.Subnet,
IPRange: config.IPRange,
Gateway: config.Gateway,
AuxAddress: config.AuxAddress,
})
}
return network.IPAM{
Driver: ipam.Driver,
Config: ipamConfigs,
}
}
// NewNetwork creates a new network from the specified name and config.
func NewNetwork(projectName, name string, config *config.NetworkConfig, client client.NetworkAPIClient) *Network {
networkName := name
if config.External.External {
networkName = config.External.Name
}
return &Network{
client: client,
name: networkName,
projectName: projectName,
driver: config.Driver,
driverOptions: config.DriverOpts,
external: config.External.External,
ipam: config.Ipam,
}
}
// Networks holds a list of network
type Networks struct {
networks []*Network
networkEnabled bool
}
// Initialize make sure network exists if network is enabled
func (n *Networks) Initialize(ctx context.Context) error {
if !n.networkEnabled {
return nil
}
for _, network := range n.networks {
err := network.EnsureItExists(ctx)
if err != nil {
return err
}
}
return nil
}
// Remove removes networks (clean-up)
func (n *Networks) Remove(ctx context.Context) error {
if !n.networkEnabled {
return nil
}
for _, network := range n.networks {
err := network.Remove(ctx)
if err != nil {
return err
}
}
return nil
}
// NetworksFromServices creates a new Networks struct based on networks configurations and
// services configuration. If a network is defined but not used by any service, it will return
// an error along the Networks.
func NetworksFromServices(cli client.NetworkAPIClient, projectName string, networkConfigs map[string]*config.NetworkConfig, services *config.ServiceConfigs, networkEnabled bool) (*Networks, error) {
var err error
networks := make([]*Network, 0, len(networkConfigs))
networkNames := map[string]*yaml.Network{}
for _, serviceName := range services.Keys() {
serviceConfig, _ := services.Get(serviceName)
if serviceConfig.NetworkMode != "" || serviceConfig.Networks == nil || len(serviceConfig.Networks.Networks) == 0 {
continue
}
for _, network := range serviceConfig.Networks.Networks {
if network.Name != "default" {
if _, ok := networkConfigs[network.Name]; !ok {
return nil, fmt.Errorf(`Service "%s" uses an undefined network "%s"`, serviceName, network.Name)
}
}
networkNames[network.Name] = network
}
}
for name, config := range networkConfigs {
network := NewNetwork(projectName, name, config, cli)
networks = append(networks, network)
}
if len(networkNames) != len(networks) {
unused := []string{}
for name := range networkConfigs {
if name == "default" {
continue
}
if _, ok := networkNames[name]; !ok {
unused = append(unused, name)
}
}
if len(unused) != 0 {
err = fmt.Errorf("Some networks were defined but are not used by any service: %v", strings.Join(unused, " "))
}
}
return &Networks{
networks: networks,
networkEnabled: networkEnabled,
}, err
}

View file

@ -0,0 +1,123 @@
package docker
import (
"os"
"path/filepath"
"golang.org/x/net/context"
"github.com/Sirupsen/logrus"
"github.com/docker/engine-api/types"
"github.com/docker/engine-api/types/filters"
"github.com/docker/libcompose/config"
"github.com/docker/libcompose/docker/client"
"github.com/docker/libcompose/docker/network"
"github.com/docker/libcompose/labels"
"github.com/docker/libcompose/lookup"
"github.com/docker/libcompose/project"
)
// ComposeVersion is name of docker-compose.yml file syntax supported version
const ComposeVersion = "1.5.0"
// NewProject creates a Project with the specified context.
func NewProject(context *Context, parseOptions *config.ParseOptions) (project.APIProject, error) {
if context.ResourceLookup == nil {
context.ResourceLookup = &lookup.FileConfigLookup{}
}
if context.EnvironmentLookup == nil {
cwd, err := os.Getwd()
if err != nil {
return nil, err
}
context.EnvironmentLookup = &lookup.ComposableEnvLookup{
Lookups: []config.EnvironmentLookup{
&lookup.EnvfileLookup{
Path: filepath.Join(cwd, ".env"),
},
&lookup.OsEnvLookup{},
},
}
}
if context.AuthLookup == nil {
context.AuthLookup = NewConfigAuthLookup(context)
}
if context.ServiceFactory == nil {
context.ServiceFactory = &ServiceFactory{
context: context,
}
}
if context.ClientFactory == nil {
factory, err := client.NewDefaultFactory(client.Options{})
if err != nil {
return nil, err
}
context.ClientFactory = factory
}
if context.NetworksFactory == nil {
networksFactory := &network.DockerFactory{
ClientFactory: context.ClientFactory,
}
context.NetworksFactory = networksFactory
}
// FIXME(vdemeester) Remove the context duplication ?
runtime := &Project{
clientFactory: context.ClientFactory,
}
p := project.NewProject(&context.Context, runtime, parseOptions)
err := p.Parse()
if err != nil {
return nil, err
}
if err = context.open(); err != nil {
logrus.Errorf("Failed to open project %s: %v", p.Name, err)
return nil, err
}
return p, err
}
// Project implements project.RuntimeProject and define docker runtime specific methods.
type Project struct {
clientFactory client.Factory
}
// RemoveOrphans implements project.RuntimeProject.RemoveOrphans.
// It will remove orphan containers that are part of the project but not to any services.
func (p *Project) RemoveOrphans(ctx context.Context, projectName string, serviceConfigs *config.ServiceConfigs) error {
client := p.clientFactory.Create(nil)
filter := filters.NewArgs()
filter.Add("label", labels.PROJECT.EqString(projectName))
containers, err := client.ContainerList(ctx, types.ContainerListOptions{
Filter: filter,
})
if err != nil {
return err
}
currentServices := map[string]struct{}{}
for _, serviceName := range serviceConfigs.Keys() {
currentServices[serviceName] = struct{}{}
}
for _, container := range containers {
serviceLabel := container.Labels[labels.SERVICE.Str()]
if _, ok := currentServices[serviceLabel]; !ok {
if err := client.ContainerKill(ctx, container.ID, "SIGKILL"); err != nil {
return err
}
if err := client.ContainerRemove(ctx, container.ID, types.ContainerRemoveOptions{
Force: true,
}); err != nil {
return err
}
}
}
return nil
}

View file

@ -0,0 +1,596 @@
package docker
import (
"fmt"
"strconv"
"strings"
"time"
"golang.org/x/net/context"
"github.com/Sirupsen/logrus"
"github.com/docker/engine-api/client"
"github.com/docker/engine-api/types"
eventtypes "github.com/docker/engine-api/types/events"
"github.com/docker/engine-api/types/filters"
"github.com/docker/go-connections/nat"
"github.com/docker/libcompose/config"
"github.com/docker/libcompose/docker/builder"
composeclient "github.com/docker/libcompose/docker/client"
"github.com/docker/libcompose/labels"
"github.com/docker/libcompose/project"
"github.com/docker/libcompose/project/events"
"github.com/docker/libcompose/project/options"
"github.com/docker/libcompose/utils"
dockerevents "github.com/vdemeester/docker-events"
)
// Service is a project.Service implementations.
type Service struct {
name string
project *project.Project
serviceConfig *config.ServiceConfig
clientFactory composeclient.Factory
authLookup AuthLookup
// FIXME(vdemeester) remove this at some point
context *Context
}
// NewService creates a service
func NewService(name string, serviceConfig *config.ServiceConfig, context *Context) *Service {
return &Service{
name: name,
project: context.Project,
serviceConfig: serviceConfig,
clientFactory: context.ClientFactory,
authLookup: context.AuthLookup,
context: context,
}
}
// Name returns the service name.
func (s *Service) Name() string {
return s.name
}
// Config returns the configuration of the service (config.ServiceConfig).
func (s *Service) Config() *config.ServiceConfig {
return s.serviceConfig
}
// DependentServices returns the dependent services (as an array of ServiceRelationship) of the service.
func (s *Service) DependentServices() []project.ServiceRelationship {
return DefaultDependentServices(s.project, s)
}
// Create implements Service.Create. It ensures the image exists or build it
// if it can and then create a container.
func (s *Service) Create(ctx context.Context, options options.Create) error {
containers, err := s.collectContainers(ctx)
if err != nil {
return err
}
imageName, err := s.ensureImageExists(ctx, options.NoBuild)
if err != nil {
return err
}
if len(containers) != 0 {
return s.eachContainer(ctx, containers, func(c *Container) error {
return s.recreateIfNeeded(ctx, imageName, c, options.NoRecreate, options.ForceRecreate)
})
}
_, err = s.createOne(ctx, imageName)
return err
}
func (s *Service) collectContainers(ctx context.Context) ([]*Container, error) {
client := s.clientFactory.Create(s)
containers, err := GetContainersByFilter(ctx, client, labels.SERVICE.Eq(s.name), labels.PROJECT.Eq(s.project.Name))
if err != nil {
return nil, err
}
result := []*Container{}
for _, container := range containers {
containerNumber, err := strconv.Atoi(container.Labels[labels.NUMBER.Str()])
if err != nil {
return nil, err
}
// Compose add "/" before name, so Name[1] will store actaul name.
name := strings.SplitAfter(container.Names[0], "/")
result = append(result, NewContainer(client, name[1], containerNumber, s))
}
return result, nil
}
func (s *Service) createOne(ctx context.Context, imageName string) (*Container, error) {
containers, err := s.constructContainers(ctx, imageName, 1)
if err != nil {
return nil, err
}
return containers[0], err
}
func (s *Service) ensureImageExists(ctx context.Context, noBuild bool) (string, error) {
exists, err := s.ImageExists(ctx)
if err != nil {
return "", err
}
if exists {
return s.imageName(), nil
}
if s.Config().Build.Context != "" {
if noBuild {
return "", fmt.Errorf("Service %q needs to be built, but no-build was specified", s.name)
}
return s.imageName(), s.build(ctx, options.Build{})
}
return s.imageName(), s.Pull(ctx)
}
// ImageExists returns whether or not the service image already exists
func (s *Service) ImageExists(ctx context.Context) (bool, error) {
dockerClient := s.clientFactory.Create(s)
_, _, err := dockerClient.ImageInspectWithRaw(ctx, s.imageName(), false)
if err == nil {
return true, nil
}
if err != nil && client.IsErrImageNotFound(err) {
return false, nil
}
return false, err
}
func (s *Service) imageName() string {
if s.Config().Image != "" {
return s.Config().Image
}
return fmt.Sprintf("%s_%s", s.project.Name, s.Name())
}
// Build implements Service.Build. It will try to build the image and returns an error if any.
func (s *Service) Build(ctx context.Context, buildOptions options.Build) error {
return s.build(ctx, buildOptions)
}
func (s *Service) build(ctx context.Context, buildOptions options.Build) error {
if s.Config().Build.Context == "" {
return fmt.Errorf("Specified service does not have a build section")
}
builder := &builder.DaemonBuilder{
Client: s.clientFactory.Create(s),
ContextDirectory: s.Config().Build.Context,
Dockerfile: s.Config().Build.Dockerfile,
BuildArgs: s.Config().Build.Args,
AuthConfigs: s.authLookup.All(),
NoCache: buildOptions.NoCache,
ForceRemove: buildOptions.ForceRemove,
Pull: buildOptions.Pull,
}
return builder.Build(ctx, s.imageName())
}
func (s *Service) constructContainers(ctx context.Context, imageName string, count int) ([]*Container, error) {
result, err := s.collectContainers(ctx)
if err != nil {
return nil, err
}
client := s.clientFactory.Create(s)
var namer Namer
if s.serviceConfig.ContainerName != "" {
if count > 1 {
logrus.Warnf(`The "%s" service is using the custom container name "%s". Docker requires each container to have a unique name. Remove the custom name to scale the service.`, s.name, s.serviceConfig.ContainerName)
}
namer = NewSingleNamer(s.serviceConfig.ContainerName)
} else {
namer, err = NewNamer(ctx, client, s.project.Name, s.name, false)
if err != nil {
return nil, err
}
}
for i := len(result); i < count; i++ {
containerName, containerNumber := namer.Next()
c := NewContainer(client, containerName, containerNumber, s)
dockerContainer, err := c.Create(ctx, imageName)
if err != nil {
return nil, err
}
logrus.Debugf("Created container %s: %v", dockerContainer.ID, dockerContainer.Name)
result = append(result, NewContainer(client, containerName, containerNumber, s))
}
return result, nil
}
// Up implements Service.Up. It builds the image if needed, creates a container
// and start it.
func (s *Service) Up(ctx context.Context, options options.Up) error {
containers, err := s.collectContainers(ctx)
if err != nil {
return err
}
var imageName = s.imageName()
if len(containers) == 0 || !options.NoRecreate {
imageName, err = s.ensureImageExists(ctx, options.NoBuild)
if err != nil {
return err
}
}
return s.up(ctx, imageName, true, options)
}
// Run implements Service.Run. It runs a one of command within the service container.
// It always create a new container.
func (s *Service) Run(ctx context.Context, commandParts []string, options options.Run) (int, error) {
imageName, err := s.ensureImageExists(ctx, false)
if err != nil {
return -1, err
}
client := s.clientFactory.Create(s)
namer, err := NewNamer(ctx, client, s.project.Name, s.name, true)
if err != nil {
return -1, err
}
containerName, containerNumber := namer.Next()
c := NewOneOffContainer(client, containerName, containerNumber, s)
configOverride := &config.ServiceConfig{Command: commandParts, Tty: true, StdinOpen: true}
c.CreateWithOverride(ctx, imageName, configOverride)
if err := s.connectContainerToNetworks(ctx, c); err != nil {
return -1, err
}
if options.Detached {
logrus.Infof("%s", c.Name())
return 0, c.Start(ctx)
}
return c.Run(ctx, configOverride)
}
// Info implements Service.Info. It returns an project.InfoSet with the containers
// related to this service (can be multiple if using the scale command).
func (s *Service) Info(ctx context.Context, qFlag bool) (project.InfoSet, error) {
result := project.InfoSet{}
containers, err := s.collectContainers(ctx)
if err != nil {
return nil, err
}
for _, c := range containers {
info, err := c.Info(ctx, qFlag)
if err != nil {
return nil, err
}
result = append(result, info)
}
return result, nil
}
// Start implements Service.Start. It tries to start a container without creating it.
func (s *Service) Start(ctx context.Context) error {
return s.collectContainersAndDo(ctx, func(c *Container) error {
if err := s.connectContainerToNetworks(ctx, c); err != nil {
return err
}
return c.Start(ctx)
})
}
func (s *Service) up(ctx context.Context, imageName string, create bool, options options.Up) error {
containers, err := s.collectContainers(ctx)
if err != nil {
return err
}
logrus.Debugf("Found %d existing containers for service %s", len(containers), s.name)
if len(containers) == 0 && create {
c, err := s.createOne(ctx, imageName)
if err != nil {
return err
}
containers = []*Container{c}
}
return s.eachContainer(ctx, containers, func(c *Container) error {
if create {
if err := s.recreateIfNeeded(ctx, imageName, c, options.NoRecreate, options.ForceRecreate); err != nil {
return err
}
}
if err := s.connectContainerToNetworks(ctx, c); err != nil {
return err
}
return c.Start(ctx)
})
}
func (s *Service) connectContainerToNetworks(ctx context.Context, c *Container) error {
connectedNetworks, err := c.Networks(ctx)
if err != nil {
return nil
}
if s.serviceConfig.Networks != nil {
for _, network := range s.serviceConfig.Networks.Networks {
existingNetwork, ok := connectedNetworks[network.Name]
if ok {
// FIXME(vdemeester) implement alias checking (to not disconnect/reconnect for nothing)
aliasPresent := false
for _, alias := range existingNetwork.Aliases {
// FIXME(vdemeester) use shortID instead of ID
ID, _ := c.ID()
if alias == ID {
aliasPresent = true
}
}
if aliasPresent {
continue
}
if err := c.NetworkDisconnect(ctx, network); err != nil {
return err
}
}
if err := c.NetworkConnect(ctx, network); err != nil {
return err
}
}
}
return nil
}
func (s *Service) recreateIfNeeded(ctx context.Context, imageName string, c *Container, noRecreate, forceRecreate bool) error {
if noRecreate {
return nil
}
outOfSync, err := c.OutOfSync(ctx, imageName)
if err != nil {
return err
}
logrus.WithFields(logrus.Fields{
"outOfSync": outOfSync,
"ForceRecreate": forceRecreate,
"NoRecreate": noRecreate}).Debug("Going to decide if recreate is needed")
if forceRecreate || outOfSync {
logrus.Infof("Recreating %s", s.name)
if _, err := c.Recreate(ctx, imageName); err != nil {
return err
}
}
return nil
}
func (s *Service) collectContainersAndDo(ctx context.Context, action func(*Container) error) error {
containers, err := s.collectContainers(ctx)
if err != nil {
return err
}
return s.eachContainer(ctx, containers, action)
}
func (s *Service) eachContainer(ctx context.Context, containers []*Container, action func(*Container) error) error {
tasks := utils.InParallel{}
for _, container := range containers {
task := func(container *Container) func() error {
return func() error {
return action(container)
}
}(container)
tasks.Add(task)
}
return tasks.Wait()
}
// Stop implements Service.Stop. It stops any containers related to the service.
func (s *Service) Stop(ctx context.Context, timeout int) error {
return s.collectContainersAndDo(ctx, func(c *Container) error {
return c.Stop(ctx, timeout)
})
}
// Restart implements Service.Restart. It restarts any containers related to the service.
func (s *Service) Restart(ctx context.Context, timeout int) error {
return s.collectContainersAndDo(ctx, func(c *Container) error {
return c.Restart(ctx, timeout)
})
}
// Kill implements Service.Kill. It kills any containers related to the service.
func (s *Service) Kill(ctx context.Context, signal string) error {
return s.collectContainersAndDo(ctx, func(c *Container) error {
return c.Kill(ctx, signal)
})
}
// Delete implements Service.Delete. It removes any containers related to the service.
func (s *Service) Delete(ctx context.Context, options options.Delete) error {
return s.collectContainersAndDo(ctx, func(c *Container) error {
return c.Delete(ctx, options.RemoveVolume)
})
}
// Log implements Service.Log. It returns the docker logs for each container related to the service.
func (s *Service) Log(ctx context.Context, follow bool) error {
return s.collectContainersAndDo(ctx, func(c *Container) error {
return c.Log(ctx, follow)
})
}
// Scale implements Service.Scale. It creates or removes containers to have the specified number
// of related container to the service to run.
func (s *Service) Scale(ctx context.Context, scale int, timeout int) error {
if s.specificiesHostPort() {
logrus.Warnf("The \"%s\" service specifies a port on the host. If multiple containers for this service are created on a single host, the port will clash.", s.Name())
}
foundCount := 0
err := s.collectContainersAndDo(ctx, func(c *Container) error {
foundCount++
if foundCount > scale {
err := c.Stop(ctx, timeout)
if err != nil {
return err
}
// FIXME(vdemeester) remove volume in scale by default ?
return c.Delete(ctx, false)
}
return nil
})
if err != nil {
return err
}
if foundCount != scale {
imageName, err := s.ensureImageExists(ctx, false)
if err != nil {
return err
}
if _, err = s.constructContainers(ctx, imageName, scale); err != nil {
return err
}
}
return s.up(ctx, "", false, options.Up{})
}
// Pull implements Service.Pull. It pulls the image of the service and skip the service that
// would need to be built.
func (s *Service) Pull(ctx context.Context) error {
if s.Config().Image == "" {
return nil
}
return pullImage(ctx, s.clientFactory.Create(s), s, s.Config().Image)
}
// Pause implements Service.Pause. It puts into pause the container(s) related
// to the service.
func (s *Service) Pause(ctx context.Context) error {
return s.collectContainersAndDo(ctx, func(c *Container) error {
return c.Pause(ctx)
})
}
// Unpause implements Service.Pause. It brings back from pause the container(s)
// related to the service.
func (s *Service) Unpause(ctx context.Context) error {
return s.collectContainersAndDo(ctx, func(c *Container) error {
return c.Unpause(ctx)
})
}
// RemoveImage implements Service.RemoveImage. It removes images used for the service
// depending on the specified type.
func (s *Service) RemoveImage(ctx context.Context, imageType options.ImageType) error {
switch imageType {
case "local":
if s.Config().Image != "" {
return nil
}
return removeImage(ctx, s.clientFactory.Create(s), s.imageName())
case "all":
return removeImage(ctx, s.clientFactory.Create(s), s.imageName())
default:
// Don't do a thing, should be validated up-front
return nil
}
}
var eventAttributes = []string{"image", "name"}
// Events implements Service.Events. It listen to all real-time events happening
// for the service, and put them into the specified chan.
func (s *Service) Events(ctx context.Context, evts chan events.ContainerEvent) error {
filter := filters.NewArgs()
filter.Add("label", fmt.Sprintf("%s=%s", labels.PROJECT, s.project.Name))
filter.Add("label", fmt.Sprintf("%s=%s", labels.SERVICE, s.name))
client := s.clientFactory.Create(s)
return <-dockerevents.Monitor(ctx, client, types.EventsOptions{
Filters: filter,
}, func(m eventtypes.Message) {
service := m.Actor.Attributes[labels.SERVICE.Str()]
attributes := map[string]string{}
for _, attr := range eventAttributes {
attributes[attr] = m.Actor.Attributes[attr]
}
e := events.ContainerEvent{
Service: service,
Event: m.Action,
Type: m.Type,
ID: m.Actor.ID,
Time: time.Unix(m.Time, 0),
Attributes: attributes,
}
evts <- e
})
}
// Containers implements Service.Containers. It returns the list of containers
// that are related to the service.
func (s *Service) Containers(ctx context.Context) ([]project.Container, error) {
result := []project.Container{}
containers, err := s.collectContainers(ctx)
if err != nil {
return nil, err
}
for _, c := range containers {
result = append(result, c)
}
return result, nil
}
func (s *Service) specificiesHostPort() bool {
_, bindings, err := nat.ParsePortSpecs(s.Config().Ports)
if err != nil {
fmt.Println(err)
}
for _, portBindings := range bindings {
for _, portBinding := range portBindings {
if portBinding.HostPort != "" {
return true
}
}
}
return false
}

View file

@ -0,0 +1,16 @@
package docker
import (
"github.com/docker/libcompose/config"
"github.com/docker/libcompose/project"
)
// ServiceFactory is an implementation of project.ServiceFactory.
type ServiceFactory struct {
context *Context
}
// Create creates a Service based on the specified project, name and service configuration.
func (s *ServiceFactory) Create(project *project.Project, name string, serviceConfig *config.ServiceConfig) (project.Service, error) {
return NewService(name, serviceConfig, s.context), nil
}

View file

@ -0,0 +1,45 @@
package docker
import (
"github.com/docker/engine-api/types/container"
"github.com/docker/libcompose/project"
)
// DefaultDependentServices return the dependent services (as an array of ServiceRelationship)
// for the specified project and service. It looks for : links, volumesFrom, net and ipc configuration.
// It uses default project implementation and append some docker specific ones.
func DefaultDependentServices(p *project.Project, s project.Service) []project.ServiceRelationship {
result := project.DefaultDependentServices(p, s)
result = appendNs(p, result, s.Config().NetworkMode, project.RelTypeNetNamespace)
result = appendNs(p, result, s.Config().Ipc, project.RelTypeIpcNamespace)
return result
}
func appendNs(p *project.Project, rels []project.ServiceRelationship, conf string, relType project.ServiceRelationshipType) []project.ServiceRelationship {
service := GetContainerFromIpcLikeConfig(p, conf)
if service != "" {
rels = append(rels, project.NewServiceRelationship(service, relType))
}
return rels
}
// GetContainerFromIpcLikeConfig returns name of the service that shares the IPC
// namespace with the specified service.
func GetContainerFromIpcLikeConfig(p *project.Project, conf string) string {
ipc := container.IpcMode(conf)
if !ipc.IsContainer() {
return ""
}
name := ipc.Container()
if name == "" {
return ""
}
if p.ServiceConfigs.Has(name) {
return name
}
return ""
}

View file

@ -0,0 +1,94 @@
package labels
import (
"encoding/json"
"fmt"
"github.com/docker/libcompose/utils"
)
// Label represents a docker label.
type Label string
// Libcompose default labels.
const (
NUMBER = Label("com.docker.compose.container-number")
ONEOFF = Label("com.docker.compose.oneoff")
PROJECT = Label("com.docker.compose.project")
SERVICE = Label("com.docker.compose.service")
HASH = Label("com.docker.compose.config-hash")
VERSION = Label("com.docker.compose.version")
)
// EqString returns a label json string representation with the specified value.
func (f Label) EqString(value string) string {
return LabelFilterString(string(f), value)
}
// Eq returns a label map representation with the specified value.
func (f Label) Eq(value string) map[string][]string {
return LabelFilter(string(f), value)
}
// AndString returns a json list of labels by merging the two specified values (left and right) serialized as string.
func AndString(left, right string) string {
leftMap := map[string][]string{}
rightMap := map[string][]string{}
// Ignore errors
json.Unmarshal([]byte(left), &leftMap)
json.Unmarshal([]byte(right), &rightMap)
for k, v := range rightMap {
existing, ok := leftMap[k]
if ok {
leftMap[k] = append(existing, v...)
} else {
leftMap[k] = v
}
}
result, _ := json.Marshal(leftMap)
return string(result)
}
// And returns a map of labels by merging the two specified values (left and right).
func And(left, right map[string][]string) map[string][]string {
result := map[string][]string{}
for k, v := range left {
result[k] = v
}
for k, v := range right {
existing, ok := result[k]
if ok {
result[k] = append(existing, v...)
} else {
result[k] = v
}
}
return result
}
// Str returns the label name.
func (f Label) Str() string {
return string(f)
}
// LabelFilterString returns a label json string representation of the specifed couple (key,value)
// that is used as filter for docker.
func LabelFilterString(key, value string) string {
return utils.FilterString(map[string][]string{
"label": {fmt.Sprintf("%s=%s", key, value)},
})
}
// LabelFilter returns a label map representation of the specifed couple (key,value)
// that is used as filter for docker.
func LabelFilter(key, value string) map[string][]string {
return map[string][]string{
"label": {fmt.Sprintf("%s=%s", key, value)},
}
}

View file

@ -0,0 +1,18 @@
package logger
// NullLogger is a logger.Logger and logger.Factory implementation that does nothing.
type NullLogger struct {
}
// Out is a no-op function.
func (n *NullLogger) Out(_ []byte) {
}
// Err is a no-op function.
func (n *NullLogger) Err(_ []byte) {
}
// Create implements logger.Factory and returns a NullLogger.
func (n *NullLogger) Create(_ string) Logger {
return &NullLogger{}
}

View file

@ -0,0 +1,29 @@
package logger
// Factory defines methods a factory should implement, to create a Logger
// based on the specified name.
type Factory interface {
Create(name string) Logger
}
// Logger defines methods to implement for being a logger.
type Logger interface {
Out(bytes []byte)
Err(bytes []byte)
}
// Wrapper is a wrapper around Logger that implements the Writer interface,
// mainly use by docker/pkg/stdcopy functions.
type Wrapper struct {
Err bool
Logger Logger
}
func (l *Wrapper) Write(bytes []byte) (int, error) {
if l.Err {
l.Logger.Err(bytes)
} else {
l.Logger.Out(bytes)
}
return len(bytes), nil
}

View file

@ -0,0 +1,25 @@
package lookup
import (
"github.com/docker/libcompose/config"
)
// ComposableEnvLookup is a structure that implements the project.EnvironmentLookup interface.
// It holds an ordered list of EnvironmentLookup to call to look for the environment value.
type ComposableEnvLookup struct {
Lookups []config.EnvironmentLookup
}
// Lookup creates a string slice of string containing a "docker-friendly" environment string
// in the form of 'key=value'. It loop through the lookups and returns the latest value if
// more than one lookup return a result.
func (l *ComposableEnvLookup) Lookup(key, serviceName string, config *config.ServiceConfig) []string {
result := []string{}
for _, lookup := range l.Lookups {
env := lookup.Lookup(key, serviceName, config)
if len(env) == 1 {
result = env
}
}
return result
}

View file

@ -0,0 +1,31 @@
package lookup
import (
"strings"
"github.com/docker/docker/runconfig/opts"
"github.com/docker/libcompose/config"
)
// EnvfileLookup is a structure that implements the project.EnvironmentLookup interface.
// It holds the path of the file where to lookup environment values.
type EnvfileLookup struct {
Path string
}
// Lookup creates a string slice of string containing a "docker-friendly" environment string
// in the form of 'key=value'. It gets environment values using a '.env' file in the specified
// path.
func (l *EnvfileLookup) Lookup(key, serviceName string, config *config.ServiceConfig) []string {
envs, err := opts.ParseEnvFile(l.Path)
if err != nil {
return []string{}
}
for _, env := range envs {
e := strings.Split(env, "=")
if e[0] == key {
return []string{env}
}
}
return []string{}
}

View file

@ -0,0 +1,66 @@
package lookup
import (
"io/ioutil"
"os"
"path"
"path/filepath"
"strings"
"github.com/Sirupsen/logrus"
)
// relativePath returns the proper relative path for the given file path. If
// the relativeTo string equals "-", then it means that it's from the stdin,
// and the returned path will be the current working directory. Otherwise, if
// file is really an absolute path, then it will be returned without any
// changes. Otherwise, the returned path will be a combination of relativeTo
// and file.
func relativePath(file, relativeTo string) string {
// stdin: return the current working directory if possible.
if relativeTo == "-" {
if cwd, err := os.Getwd(); err == nil {
return cwd
}
}
// If the given file is already an absolute path, just return it.
// Otherwise, the returned path will be relative to the given relativeTo
// path.
if filepath.IsAbs(file) {
return file
}
abs, err := filepath.Abs(filepath.Join(path.Dir(relativeTo), file))
if err != nil {
logrus.Errorf("Failed to get absolute directory: %s", err)
return file
}
return abs
}
// FileConfigLookup is a "bare" structure that implements the project.ResourceLookup interface
type FileConfigLookup struct {
}
// Lookup returns the content and the actual filename of the file that is "built" using the
// specified file and relativeTo string. file and relativeTo are supposed to be file path.
// If file starts with a slash ('/'), it tries to load it, otherwise it will build a
// filename using the folder part of relativeTo joined with file.
func (f *FileConfigLookup) Lookup(file, relativeTo string) ([]byte, string, error) {
file = relativePath(file, relativeTo)
logrus.Debugf("Reading file %s", file)
bytes, err := ioutil.ReadFile(file)
return bytes, file, err
}
// ResolvePath returns the path to be used for the given path volume. This
// function already takes care of relative paths.
func (f *FileConfigLookup) ResolvePath(path, inFile string) string {
vs := strings.SplitN(path, ":", 2)
if len(vs) != 2 || filepath.IsAbs(vs[0]) {
return path
}
vs[0] = relativePath(vs[0], inFile)
return strings.Join(vs, ":")
}

View file

@ -0,0 +1,24 @@
package lookup
import (
"fmt"
"os"
"github.com/docker/libcompose/config"
)
// OsEnvLookup is a "bare" structure that implements the project.EnvironmentLookup interface
type OsEnvLookup struct {
}
// Lookup creates a string slice of string containing a "docker-friendly" environment string
// in the form of 'key=value'. It gets environment values using os.Getenv.
// If the os environment variable does not exists, the slice is empty. serviceName and config
// are not used at all in this implementation.
func (o *OsEnvLookup) Lookup(key, serviceName string, config *config.ServiceConfig) []string {
ret := os.Getenv(key)
if ret == "" {
return []string{}
}
return []string{fmt.Sprintf("%s=%s", key, ret)}
}

View file

@ -0,0 +1 @@
package libcompose

View file

@ -0,0 +1,13 @@
package project
import (
"golang.org/x/net/context"
)
// Container defines what a libcompose container provides.
type Container interface {
ID() (string, error)
Name() string
Port(ctx context.Context, port string) (string, error)
IsRunning(ctx context.Context) (bool, error)
}

View file

@ -0,0 +1,140 @@
package project
import (
"fmt"
"io/ioutil"
"os"
"path"
"path/filepath"
"regexp"
"strings"
"github.com/Sirupsen/logrus"
"github.com/docker/libcompose/config"
"github.com/docker/libcompose/logger"
)
var projectRegexp = regexp.MustCompile("[^a-zA-Z0-9_.-]")
// Context holds context meta information about a libcompose project, like
// the project name, the compose file, etc.
type Context struct {
ComposeFiles []string
ComposeBytes [][]byte
ProjectName string
isOpen bool
ServiceFactory ServiceFactory
NetworksFactory NetworksFactory
EnvironmentLookup config.EnvironmentLookup
ResourceLookup config.ResourceLookup
LoggerFactory logger.Factory
IgnoreMissingConfig bool
Project *Project
}
func (c *Context) readComposeFiles() error {
if c.ComposeBytes != nil {
return nil
}
logrus.Debugf("Opening compose files: %s", strings.Join(c.ComposeFiles, ","))
// Handle STDIN (`-f -`)
if len(c.ComposeFiles) == 1 && c.ComposeFiles[0] == "-" {
composeBytes, err := ioutil.ReadAll(os.Stdin)
if err != nil {
logrus.Errorf("Failed to read compose file from stdin: %v", err)
return err
}
c.ComposeBytes = [][]byte{composeBytes}
return nil
}
for _, composeFile := range c.ComposeFiles {
composeBytes, err := ioutil.ReadFile(composeFile)
if err != nil && !os.IsNotExist(err) {
logrus.Errorf("Failed to open the compose file: %s", composeFile)
return err
}
if err != nil && !c.IgnoreMissingConfig {
logrus.Errorf("Failed to find the compose file: %s", composeFile)
return err
}
c.ComposeBytes = append(c.ComposeBytes, composeBytes)
}
return nil
}
func (c *Context) determineProject() error {
name, err := c.lookupProjectName()
if err != nil {
return err
}
c.ProjectName = normalizeName(name)
if c.ProjectName == "" {
return fmt.Errorf("Falied to determine project name")
}
return nil
}
func (c *Context) lookupProjectName() (string, error) {
if c.ProjectName != "" {
return c.ProjectName, nil
}
if envProject := os.Getenv("COMPOSE_PROJECT_NAME"); envProject != "" {
return envProject, nil
}
file := "."
if len(c.ComposeFiles) > 0 {
file = c.ComposeFiles[0]
}
f, err := filepath.Abs(file)
if err != nil {
logrus.Errorf("Failed to get absolute directory for: %s", file)
return "", err
}
f = toUnixPath(f)
parent := path.Base(path.Dir(f))
if parent != "" && parent != "." {
return parent, nil
} else if wd, err := os.Getwd(); err != nil {
return "", err
} else {
return path.Base(toUnixPath(wd)), nil
}
}
func normalizeName(name string) string {
r := regexp.MustCompile("[^a-z0-9]+")
return r.ReplaceAllString(strings.ToLower(name), "")
}
func toUnixPath(p string) string {
return strings.Replace(p, "\\", "/", -1)
}
func (c *Context) open() error {
if c.isOpen {
return nil
}
if err := c.readComposeFiles(); err != nil {
return err
}
if err := c.determineProject(); err != nil {
return err
}
c.isOpen = true
return nil
}

View file

@ -0,0 +1,122 @@
package project
import (
"golang.org/x/net/context"
"github.com/docker/libcompose/config"
"github.com/docker/libcompose/project/events"
"github.com/docker/libcompose/project/options"
)
// this ensures EmptyService implements Service
// useful since it's easy to forget adding new functions to EmptyService
var _ Service = &EmptyService{}
// EmptyService is a struct that implements Service but does nothing.
type EmptyService struct {
}
// Create implements Service.Create but does nothing.
func (e *EmptyService) Create(ctx context.Context, options options.Create) error {
return nil
}
// Build implements Service.Build but does nothing.
func (e *EmptyService) Build(ctx context.Context, buildOptions options.Build) error {
return nil
}
// Up implements Service.Up but does nothing.
func (e *EmptyService) Up(ctx context.Context, options options.Up) error {
return nil
}
// Start implements Service.Start but does nothing.
func (e *EmptyService) Start(ctx context.Context) error {
return nil
}
// Stop implements Service.Stop() but does nothing.
func (e *EmptyService) Stop(ctx context.Context, timeout int) error {
return nil
}
// Delete implements Service.Delete but does nothing.
func (e *EmptyService) Delete(ctx context.Context, options options.Delete) error {
return nil
}
// Restart implements Service.Restart but does nothing.
func (e *EmptyService) Restart(ctx context.Context, timeout int) error {
return nil
}
// Log implements Service.Log but does nothing.
func (e *EmptyService) Log(ctx context.Context, follow bool) error {
return nil
}
// Pull implements Service.Pull but does nothing.
func (e *EmptyService) Pull(ctx context.Context) error {
return nil
}
// Kill implements Service.Kill but does nothing.
func (e *EmptyService) Kill(ctx context.Context, signal string) error {
return nil
}
// Containers implements Service.Containers but does nothing.
func (e *EmptyService) Containers(ctx context.Context) ([]Container, error) {
return []Container{}, nil
}
// Scale implements Service.Scale but does nothing.
func (e *EmptyService) Scale(ctx context.Context, count int, timeout int) error {
return nil
}
// Info implements Service.Info but does nothing.
func (e *EmptyService) Info(ctx context.Context, qFlag bool) (InfoSet, error) {
return InfoSet{}, nil
}
// Pause implements Service.Pause but does nothing.
func (e *EmptyService) Pause(ctx context.Context) error {
return nil
}
// Unpause implements Service.Pause but does nothing.
func (e *EmptyService) Unpause(ctx context.Context) error {
return nil
}
// Run implements Service.Run but does nothing.
func (e *EmptyService) Run(ctx context.Context, commandParts []string, options options.Run) (int, error) {
return 0, nil
}
// RemoveImage implements Service.RemoveImage but does nothing.
func (e *EmptyService) RemoveImage(ctx context.Context, imageType options.ImageType) error {
return nil
}
// Events implements Service.Events but does nothing.
func (e *EmptyService) Events(ctx context.Context, events chan events.ContainerEvent) error {
return nil
}
// DependentServices implements Service.DependentServices with empty slice.
func (e *EmptyService) DependentServices() []ServiceRelationship {
return []ServiceRelationship{}
}
// Config implements Service.Config with empty config.
func (e *EmptyService) Config() *config.ServiceConfig {
return &config.ServiceConfig{}
}
// Name implements Service.Name with empty name.
func (e *EmptyService) Name() string {
return ""
}

View file

@ -0,0 +1,224 @@
// Package events holds event structures, methods and functions.
package events
import (
"fmt"
"time"
)
// Notifier defines the methods an event notifier should have.
type Notifier interface {
Notify(eventType EventType, serviceName string, data map[string]string)
}
// Emitter defines the methods an event emitter should have.
type Emitter interface {
AddListener(c chan<- Event)
}
// Event holds project-wide event informations.
type Event struct {
EventType EventType
ServiceName string
Data map[string]string
}
// ContainerEvent holds attributes of container events.
type ContainerEvent struct {
Service string `json:"service"`
Event string `json:"event"`
ID string `json:"id"`
Time time.Time `json:"time"`
Attributes map[string]string `json:"attributes"`
Type string `json:"type"`
}
// EventType defines a type of libcompose event.
type EventType int
// Definitions of libcompose events
const (
NoEvent = EventType(iota)
ContainerCreated = EventType(iota)
ContainerStarted = EventType(iota)
ServiceAdd = EventType(iota)
ServiceUpStart = EventType(iota)
ServiceUpIgnored = EventType(iota)
ServiceUp = EventType(iota)
ServiceCreateStart = EventType(iota)
ServiceCreate = EventType(iota)
ServiceDeleteStart = EventType(iota)
ServiceDelete = EventType(iota)
ServiceDownStart = EventType(iota)
ServiceDown = EventType(iota)
ServiceRestartStart = EventType(iota)
ServiceRestart = EventType(iota)
ServicePullStart = EventType(iota)
ServicePull = EventType(iota)
ServiceKillStart = EventType(iota)
ServiceKill = EventType(iota)
ServiceStartStart = EventType(iota)
ServiceStart = EventType(iota)
ServiceBuildStart = EventType(iota)
ServiceBuild = EventType(iota)
ServicePauseStart = EventType(iota)
ServicePause = EventType(iota)
ServiceUnpauseStart = EventType(iota)
ServiceUnpause = EventType(iota)
ServiceStopStart = EventType(iota)
ServiceStop = EventType(iota)
ServiceRunStart = EventType(iota)
ServiceRun = EventType(iota)
VolumeAdd = EventType(iota)
NetworkAdd = EventType(iota)
ProjectDownStart = EventType(iota)
ProjectDownDone = EventType(iota)
ProjectCreateStart = EventType(iota)
ProjectCreateDone = EventType(iota)
ProjectUpStart = EventType(iota)
ProjectUpDone = EventType(iota)
ProjectDeleteStart = EventType(iota)
ProjectDeleteDone = EventType(iota)
ProjectRestartStart = EventType(iota)
ProjectRestartDone = EventType(iota)
ProjectReload = EventType(iota)
ProjectReloadTrigger = EventType(iota)
ProjectKillStart = EventType(iota)
ProjectKillDone = EventType(iota)
ProjectStartStart = EventType(iota)
ProjectStartDone = EventType(iota)
ProjectBuildStart = EventType(iota)
ProjectBuildDone = EventType(iota)
ProjectPauseStart = EventType(iota)
ProjectPauseDone = EventType(iota)
ProjectUnpauseStart = EventType(iota)
ProjectUnpauseDone = EventType(iota)
ProjectStopStart = EventType(iota)
ProjectStopDone = EventType(iota)
)
func (e EventType) String() string {
var m string
switch e {
case ContainerCreated:
m = "Created container"
case ContainerStarted:
m = "Started container"
case ServiceAdd:
m = "Adding"
case ServiceUpStart:
m = "Starting"
case ServiceUpIgnored:
m = "Ignoring"
case ServiceUp:
m = "Started"
case ServiceCreateStart:
m = "Creating"
case ServiceCreate:
m = "Created"
case ServiceDeleteStart:
m = "Deleting"
case ServiceDelete:
m = "Deleted"
case ServiceStopStart:
m = "Stopping"
case ServiceStop:
m = "Stopped"
case ServiceDownStart:
m = "Stopping"
case ServiceDown:
m = "Stopped"
case ServiceRestartStart:
m = "Restarting"
case ServiceRestart:
m = "Restarted"
case ServicePullStart:
m = "Pulling"
case ServicePull:
m = "Pulled"
case ServiceKillStart:
m = "Killing"
case ServiceKill:
m = "Killed"
case ServiceStartStart:
m = "Starting"
case ServiceStart:
m = "Started"
case ServiceBuildStart:
m = "Building"
case ServiceBuild:
m = "Built"
case ServiceRunStart:
m = "Executing"
case ServiceRun:
m = "Executed"
case ServicePauseStart:
m = "Pausing"
case ServicePause:
m = "Paused"
case ServiceUnpauseStart:
m = "Unpausing"
case ServiceUnpause:
m = "Unpaused"
case ProjectDownStart:
m = "Stopping project"
case ProjectDownDone:
m = "Project stopped"
case ProjectStopStart:
m = "Stopping project"
case ProjectStopDone:
m = "Project stopped"
case ProjectCreateStart:
m = "Creating project"
case ProjectCreateDone:
m = "Project created"
case ProjectUpStart:
m = "Starting project"
case ProjectUpDone:
m = "Project started"
case ProjectDeleteStart:
m = "Deleting project"
case ProjectDeleteDone:
m = "Project deleted"
case ProjectRestartStart:
m = "Restarting project"
case ProjectRestartDone:
m = "Project restarted"
case ProjectReload:
m = "Reloading project"
case ProjectReloadTrigger:
m = "Triggering project reload"
case ProjectKillStart:
m = "Killing project"
case ProjectKillDone:
m = "Project killed"
case ProjectStartStart:
m = "Starting project"
case ProjectStartDone:
m = "Project started"
case ProjectBuildStart:
m = "Building project"
case ProjectBuildDone:
m = "Project built"
case ProjectPauseStart:
m = "Pausing project"
case ProjectPauseDone:
m = "Project paused"
case ProjectUnpauseStart:
m = "Unpausing project"
case ProjectUnpauseDone:
m = "Project unpaused"
}
if m == "" {
m = fmt.Sprintf("EventType: %d", int(e))
}
return m
}

View file

@ -0,0 +1,53 @@
package project
import (
"bytes"
"io"
"text/tabwriter"
)
// InfoPart holds key/value strings.
type InfoPart struct {
Key, Value string
}
// InfoSet holds a list of Info.
type InfoSet []Info
// Info holds a list of InfoPart.
type Info []InfoPart
func (infos InfoSet) String(titleFlag bool) string {
//no error checking, none of this should fail
buffer := bytes.NewBuffer(make([]byte, 0, 1024))
tabwriter := tabwriter.NewWriter(buffer, 4, 4, 2, ' ', 0)
first := true
for _, info := range infos {
if first && titleFlag {
writeLine(tabwriter, true, info)
}
first = false
writeLine(tabwriter, false, info)
}
tabwriter.Flush()
return buffer.String()
}
func writeLine(writer io.Writer, key bool, info Info) {
first := true
for _, part := range info {
if !first {
writer.Write([]byte{'\t'})
}
first = false
if key {
writer.Write([]byte(part.Key))
} else {
writer.Write([]byte(part.Value))
}
}
writer.Write([]byte{'\n'})
}

View file

@ -0,0 +1,45 @@
package project
import (
"golang.org/x/net/context"
"github.com/docker/libcompose/config"
"github.com/docker/libcompose/project/events"
"github.com/docker/libcompose/project/options"
)
// APIProject defines the methods a libcompose project should implement.
type APIProject interface {
events.Notifier
events.Emitter
Build(ctx context.Context, options options.Build, sevice ...string) error
Create(ctx context.Context, options options.Create, services ...string) error
Delete(ctx context.Context, options options.Delete, services ...string) error
Down(ctx context.Context, options options.Down, services ...string) error
Events(ctx context.Context, services ...string) (chan events.ContainerEvent, error)
Kill(ctx context.Context, signal string, services ...string) error
Log(ctx context.Context, follow bool, services ...string) error
Pause(ctx context.Context, services ...string) error
Ps(ctx context.Context, onlyID bool, services ...string) (InfoSet, error)
// FIXME(vdemeester) we could use nat.Port instead ?
Port(ctx context.Context, index int, protocol, serviceName, privatePort string) (string, error)
Pull(ctx context.Context, services ...string) error
Restart(ctx context.Context, timeout int, services ...string) error
Run(ctx context.Context, serviceName string, commandParts []string, options options.Run) (int, error)
Scale(ctx context.Context, timeout int, servicesScale map[string]int) error
Start(ctx context.Context, services ...string) error
Stop(ctx context.Context, timeout int, services ...string) error
Unpause(ctx context.Context, services ...string) error
Up(ctx context.Context, options options.Up, services ...string) error
Parse() error
CreateService(name string) (Service, error)
AddConfig(name string, config *config.ServiceConfig) error
Load(bytes []byte) error
}
// RuntimeProject defines runtime-specific methods for a libcompose implementation.
type RuntimeProject interface {
RemoveOrphans(ctx context.Context, projectName string, serviceConfigs *config.ServiceConfigs) error
}

View file

@ -0,0 +1,81 @@
package project
import (
"bytes"
"github.com/Sirupsen/logrus"
"github.com/docker/libcompose/project/events"
)
var (
infoEvents = map[events.EventType]bool{
events.ServiceDeleteStart: true,
events.ServiceDelete: true,
events.ServiceDownStart: true,
events.ServiceDown: true,
events.ServiceStopStart: true,
events.ServiceStop: true,
events.ServiceKillStart: true,
events.ServiceKill: true,
events.ServiceCreateStart: true,
events.ServiceCreate: true,
events.ServiceStartStart: true,
events.ServiceStart: true,
events.ServiceRestartStart: true,
events.ServiceRestart: true,
events.ServiceUpStart: true,
events.ServiceUp: true,
events.ServicePauseStart: true,
events.ServicePause: true,
events.ServiceUnpauseStart: true,
events.ServiceUnpause: true,
}
)
type defaultListener struct {
project *Project
listenChan chan events.Event
upCount int
}
// NewDefaultListener create a default listener for the specified project.
func NewDefaultListener(p *Project) chan<- events.Event {
l := defaultListener{
listenChan: make(chan events.Event),
project: p,
}
go l.start()
return l.listenChan
}
func (d *defaultListener) start() {
for event := range d.listenChan {
buffer := bytes.NewBuffer(nil)
if event.Data != nil {
for k, v := range event.Data {
if buffer.Len() > 0 {
buffer.WriteString(", ")
}
buffer.WriteString(k)
buffer.WriteString("=")
buffer.WriteString(v)
}
}
if event.EventType == events.ServiceUp {
d.upCount++
}
logf := logrus.Debugf
if infoEvents[event.EventType] {
logf = logrus.Infof
}
if event.ServiceName == "" {
logf("Project [%s]: %s %s", d.project.Name, event.EventType, buffer.Bytes())
} else {
logf("[%d/%d] [%s]: %s %s", d.upCount, d.project.ServiceConfigs.Len(), event.ServiceName, event.EventType, buffer.Bytes())
}
}
}

View file

@ -0,0 +1,19 @@
package project
import (
"golang.org/x/net/context"
"github.com/docker/libcompose/config"
)
// Networks defines the methods a libcompose network aggregate should define.
type Networks interface {
Initialize(ctx context.Context) error
Remove(ctx context.Context) error
}
// NetworksFactory is an interface factory to create Networks object for the specified
// configurations (service, networks, …)
type NetworksFactory interface {
Create(projectName string, networkConfigs map[string]*config.NetworkConfig, serviceConfigs *config.ServiceConfigs, networkEnabled bool) (Networks, error)
}

View file

@ -0,0 +1,52 @@
package options
// Build holds options of compose build.
type Build struct {
NoCache bool
ForceRemove bool
Pull bool
}
// Delete holds options of compose rm.
type Delete struct {
RemoveVolume bool
BeforeDeleteCallback func([]string) bool
}
// Down holds options of compose down.
type Down struct {
RemoveVolume bool
RemoveImages ImageType
RemoveOrphans bool
}
// Create holds options of compose create.
type Create struct {
NoRecreate bool
ForceRecreate bool
NoBuild bool
// ForceBuild bool
}
// Run holds options of compose run.
type Run struct {
Detached bool
}
// Up holds options of compose up.
type Up struct {
Create
}
// ImageType defines the type of image (local, all)
type ImageType string
// Valid indicates whether the image type is valid.
func (i ImageType) Valid() bool {
switch string(i) {
case "", "local", "all":
return true
default:
return false
}
}

View file

@ -0,0 +1,745 @@
package project
import (
"errors"
"fmt"
"strings"
"golang.org/x/net/context"
log "github.com/Sirupsen/logrus"
"github.com/docker/libcompose/config"
"github.com/docker/libcompose/logger"
"github.com/docker/libcompose/project/events"
"github.com/docker/libcompose/project/options"
"github.com/docker/libcompose/utils"
"github.com/docker/libcompose/yaml"
)
type wrapperAction func(*serviceWrapper, map[string]*serviceWrapper)
type serviceAction func(service Service) error
// Project holds libcompose project information.
type Project struct {
Name string
ServiceConfigs *config.ServiceConfigs
VolumeConfigs map[string]*config.VolumeConfig
NetworkConfigs map[string]*config.NetworkConfig
Files []string
ReloadCallback func() error
ParseOptions *config.ParseOptions
runtime RuntimeProject
networks Networks
configVersion string
context *Context
reload []string
upCount int
listeners []chan<- events.Event
hasListeners bool
}
// NewProject creates a new project with the specified context.
func NewProject(context *Context, runtime RuntimeProject, parseOptions *config.ParseOptions) *Project {
p := &Project{
context: context,
runtime: runtime,
ParseOptions: parseOptions,
ServiceConfigs: config.NewServiceConfigs(),
VolumeConfigs: make(map[string]*config.VolumeConfig),
NetworkConfigs: make(map[string]*config.NetworkConfig),
}
if context.LoggerFactory == nil {
context.LoggerFactory = &logger.NullLogger{}
}
context.Project = p
p.listeners = []chan<- events.Event{NewDefaultListener(p)}
return p
}
// Parse populates project information based on its context. It sets up the name,
// the composefile and the composebytes (the composefile content).
func (p *Project) Parse() error {
err := p.context.open()
if err != nil {
return err
}
p.Name = p.context.ProjectName
p.Files = p.context.ComposeFiles
if len(p.Files) == 1 && p.Files[0] == "-" {
p.Files = []string{"."}
}
if p.context.ComposeBytes != nil {
for i, composeBytes := range p.context.ComposeBytes {
file := ""
if i < len(p.context.ComposeFiles) {
file = p.Files[i]
}
if err := p.load(file, composeBytes); err != nil {
return err
}
}
}
return nil
}
// CreateService creates a service with the specified name based. If there
// is no config in the project for this service, it will return an error.
func (p *Project) CreateService(name string) (Service, error) {
existing, ok := p.ServiceConfigs.Get(name)
if !ok {
return nil, fmt.Errorf("Failed to find service: %s", name)
}
// Copy because we are about to modify the environment
config := *existing
if p.context.EnvironmentLookup != nil {
parsedEnv := make([]string, 0, len(config.Environment))
for _, env := range config.Environment {
parts := strings.SplitN(env, "=", 2)
if len(parts) > 1 && parts[1] != "" {
parsedEnv = append(parsedEnv, env)
continue
} else {
env = parts[0]
}
for _, value := range p.context.EnvironmentLookup.Lookup(env, name, &config) {
parsedEnv = append(parsedEnv, value)
}
}
config.Environment = parsedEnv
// check the environment for extra build Args that are set but not given a value in the compose file
for arg, value := range config.Build.Args {
if value == "\x00" {
envValue := p.context.EnvironmentLookup.Lookup(arg, name, &config)
// depending on what we get back we do different things
switch l := len(envValue); l {
case 0:
delete(config.Build.Args, arg)
case 1:
parts := strings.SplitN(envValue[0], "=", 2)
config.Build.Args[parts[0]] = parts[1]
default:
return nil, fmt.Errorf("Tried to set Build Arg %#v to multi-value %#v.", arg, envValue)
}
}
}
}
return p.context.ServiceFactory.Create(p, name, &config)
}
// AddConfig adds the specified service config for the specified name.
func (p *Project) AddConfig(name string, config *config.ServiceConfig) error {
p.Notify(events.ServiceAdd, name, nil)
p.ServiceConfigs.Add(name, config)
p.reload = append(p.reload, name)
return nil
}
// AddVolumeConfig adds the specified volume config for the specified name.
func (p *Project) AddVolumeConfig(name string, config *config.VolumeConfig) error {
p.Notify(events.VolumeAdd, name, nil)
p.VolumeConfigs[name] = config
return nil
}
// AddNetworkConfig adds the specified network config for the specified name.
func (p *Project) AddNetworkConfig(name string, config *config.NetworkConfig) error {
p.Notify(events.NetworkAdd, name, nil)
p.NetworkConfigs[name] = config
return nil
}
// Load loads the specified byte array (the composefile content) and adds the
// service configuration to the project.
// FIXME is it needed ?
func (p *Project) Load(bytes []byte) error {
return p.load("", bytes)
}
func (p *Project) load(file string, bytes []byte) error {
version, serviceConfigs, volumeConfigs, networkConfigs, err := config.Merge(p.ServiceConfigs, p.context.EnvironmentLookup, p.context.ResourceLookup, file, bytes, p.ParseOptions)
if err != nil {
log.Errorf("Could not parse config for project %s : %v", p.Name, err)
return err
}
p.configVersion = version
for name, config := range volumeConfigs {
err := p.AddVolumeConfig(name, config)
if err != nil {
return err
}
}
for name, config := range networkConfigs {
err := p.AddNetworkConfig(name, config)
if err != nil {
return err
}
}
for name, config := range serviceConfigs {
err := p.AddConfig(name, config)
if err != nil {
return err
}
}
// Update network configuration a little bit
if p.isNetworkEnabled() {
for _, serviceName := range p.ServiceConfigs.Keys() {
serviceConfig, _ := p.ServiceConfigs.Get(serviceName)
if serviceConfig.NetworkMode != "" {
continue
}
if serviceConfig.Networks == nil || len(serviceConfig.Networks.Networks) == 0 {
// Add default as network
serviceConfig.Networks = &yaml.Networks{
Networks: []*yaml.Network{
{
Name: "default",
},
},
}
}
// Consolidate the name of the network
// FIXME(vdemeester) probably shouldn't be there, maybe move that to interface/factory
for _, network := range serviceConfig.Networks.Networks {
if net, ok := p.NetworkConfigs[network.Name]; ok {
if net.External.External {
network.RealName = network.Name
if net.External.Name != "" {
network.RealName = net.External.Name
}
} else {
network.RealName = p.Name + "_" + network.Name
}
}
// Ignoring if we don't find the network, it will be catched later
}
}
}
// FIXME(vdemeester) Not sure about this..
if p.context.NetworksFactory != nil {
networks, err := p.context.NetworksFactory.Create(p.Name, p.NetworkConfigs, p.ServiceConfigs, p.isNetworkEnabled())
if err != nil {
return err
}
p.networks = networks
}
return nil
}
func (p *Project) isNetworkEnabled() bool {
return p.configVersion == "2"
}
// initialize sets up required element for project before any action (on project and service).
// This means it's not needed to be called on Config for example.
func (p *Project) initialize(ctx context.Context) error {
if err := p.networks.Initialize(ctx); err != nil {
return err
}
// TODO Initialize volumes
return nil
}
func (p *Project) loadWrappers(wrappers map[string]*serviceWrapper, servicesToConstruct []string) error {
for _, name := range servicesToConstruct {
wrapper, err := newServiceWrapper(name, p)
if err != nil {
return err
}
wrappers[name] = wrapper
}
return nil
}
// Build builds the specified services (like docker build).
func (p *Project) Build(ctx context.Context, buildOptions options.Build, services ...string) error {
return p.perform(events.ProjectBuildStart, events.ProjectBuildDone, services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) {
wrapper.Do(wrappers, events.ServiceBuildStart, events.ServiceBuild, func(service Service) error {
return service.Build(ctx, buildOptions)
})
}), nil)
}
// Create creates the specified services (like docker create).
func (p *Project) Create(ctx context.Context, options options.Create, services ...string) error {
if options.NoRecreate && options.ForceRecreate {
return fmt.Errorf("no-recreate and force-recreate cannot be combined")
}
return p.perform(events.ProjectCreateStart, events.ProjectCreateDone, services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) {
wrapper.Do(wrappers, events.ServiceCreateStart, events.ServiceCreate, func(service Service) error {
return service.Create(ctx, options)
})
}), nil)
}
// Stop stops the specified services (like docker stop).
func (p *Project) Stop(ctx context.Context, timeout int, services ...string) error {
return p.perform(events.ProjectStopStart, events.ProjectStopDone, services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) {
wrapper.Do(nil, events.ServiceStopStart, events.ServiceStop, func(service Service) error {
return service.Stop(ctx, timeout)
})
}), nil)
}
// Down stops the specified services and clean related containers (like docker stop + docker rm).
func (p *Project) Down(ctx context.Context, opts options.Down, services ...string) error {
if !opts.RemoveImages.Valid() {
return fmt.Errorf("--rmi flag must be local, all or empty")
}
if err := p.Stop(ctx, 10, services...); err != nil {
return err
}
if opts.RemoveOrphans {
if err := p.runtime.RemoveOrphans(ctx, p.Name, p.ServiceConfigs); err != nil {
return err
}
}
if err := p.Delete(ctx, options.Delete{
RemoveVolume: opts.RemoveVolume,
}, services...); err != nil {
return err
}
networks, err := p.context.NetworksFactory.Create(p.Name, p.NetworkConfigs, p.ServiceConfigs, p.isNetworkEnabled())
if err != nil {
return err
}
if err := networks.Remove(ctx); err != nil {
return err
}
return p.forEach([]string{}, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) {
wrapper.Do(wrappers, events.NoEvent, events.NoEvent, func(service Service) error {
return service.RemoveImage(ctx, opts.RemoveImages)
})
}), func(service Service) error {
return service.Create(ctx, options.Create{})
})
}
// RemoveOrphans implements project.RuntimeProject.RemoveOrphans.
// It does nothing by default as it is supposed to be overriden by specific implementation.
func (p *Project) RemoveOrphans(ctx context.Context) error {
return nil
}
// Restart restarts the specified services (like docker restart).
func (p *Project) Restart(ctx context.Context, timeout int, services ...string) error {
return p.perform(events.ProjectRestartStart, events.ProjectRestartDone, services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) {
wrapper.Do(wrappers, events.ServiceRestartStart, events.ServiceRestart, func(service Service) error {
return service.Restart(ctx, timeout)
})
}), nil)
}
// Port returns the public port for a port binding of the specified service.
func (p *Project) Port(ctx context.Context, index int, protocol, serviceName, privatePort string) (string, error) {
service, err := p.CreateService(serviceName)
if err != nil {
return "", err
}
containers, err := service.Containers(ctx)
if err != nil {
return "", err
}
if index < 1 || index > len(containers) {
return "", fmt.Errorf("Invalid index %d", index)
}
return containers[index-1].Port(ctx, fmt.Sprintf("%s/%s", privatePort, protocol))
}
// Ps list containers for the specified services.
func (p *Project) Ps(ctx context.Context, onlyID bool, services ...string) (InfoSet, error) {
allInfo := InfoSet{}
for _, name := range p.ServiceConfigs.Keys() {
service, err := p.CreateService(name)
if err != nil {
return nil, err
}
info, err := service.Info(ctx, onlyID)
if err != nil {
return nil, err
}
allInfo = append(allInfo, info...)
}
return allInfo, nil
}
// Start starts the specified services (like docker start).
func (p *Project) Start(ctx context.Context, services ...string) error {
return p.perform(events.ProjectStartStart, events.ProjectStartDone, services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) {
wrapper.Do(wrappers, events.ServiceStartStart, events.ServiceStart, func(service Service) error {
return service.Start(ctx)
})
}), nil)
}
// Run executes a one off command (like `docker run image command`).
func (p *Project) Run(ctx context.Context, serviceName string, commandParts []string, opts options.Run) (int, error) {
if !p.ServiceConfigs.Has(serviceName) {
return 1, fmt.Errorf("%s is not defined in the template", serviceName)
}
if err := p.initialize(ctx); err != nil {
return 1, err
}
var exitCode int
err := p.forEach([]string{}, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) {
wrapper.Do(wrappers, events.ServiceRunStart, events.ServiceRun, func(service Service) error {
if service.Name() == serviceName {
code, err := service.Run(ctx, commandParts, opts)
exitCode = code
return err
}
return nil
})
}), func(service Service) error {
return service.Create(ctx, options.Create{})
})
return exitCode, err
}
// Up creates and starts the specified services (kinda like docker run).
func (p *Project) Up(ctx context.Context, options options.Up, services ...string) error {
if err := p.initialize(ctx); err != nil {
return err
}
return p.perform(events.ProjectUpStart, events.ProjectUpDone, services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) {
wrapper.Do(wrappers, events.ServiceUpStart, events.ServiceUp, func(service Service) error {
return service.Up(ctx, options)
})
}), func(service Service) error {
return service.Create(ctx, options.Create)
})
}
// Log aggregates and prints out the logs for the specified services.
func (p *Project) Log(ctx context.Context, follow bool, services ...string) error {
return p.forEach(services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) {
wrapper.Do(nil, events.NoEvent, events.NoEvent, func(service Service) error {
return service.Log(ctx, follow)
})
}), nil)
}
// Scale scales the specified services.
func (p *Project) Scale(ctx context.Context, timeout int, servicesScale map[string]int) error {
// This code is a bit verbose but I wanted to parse everything up front
order := make([]string, 0, 0)
services := make(map[string]Service)
for name := range servicesScale {
if !p.ServiceConfigs.Has(name) {
return fmt.Errorf("%s is not defined in the template", name)
}
service, err := p.CreateService(name)
if err != nil {
return fmt.Errorf("Failed to lookup service: %s: %v", service, err)
}
order = append(order, name)
services[name] = service
}
for _, name := range order {
scale := servicesScale[name]
log.Infof("Setting scale %s=%d...", name, scale)
err := services[name].Scale(ctx, scale, timeout)
if err != nil {
return fmt.Errorf("Failed to set the scale %s=%d: %v", name, scale, err)
}
}
return nil
}
// Pull pulls the specified services (like docker pull).
func (p *Project) Pull(ctx context.Context, services ...string) error {
return p.forEach(services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) {
wrapper.Do(nil, events.ServicePullStart, events.ServicePull, func(service Service) error {
return service.Pull(ctx)
})
}), nil)
}
// listStoppedContainers lists the stopped containers for the specified services.
func (p *Project) listStoppedContainers(ctx context.Context, services ...string) ([]string, error) {
stoppedContainers := []string{}
err := p.forEach(services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) {
wrapper.Do(nil, events.NoEvent, events.NoEvent, func(service Service) error {
containers, innerErr := service.Containers(ctx)
if innerErr != nil {
return innerErr
}
for _, container := range containers {
running, innerErr := container.IsRunning(ctx)
if innerErr != nil {
log.Error(innerErr)
}
if !running {
containerID, innerErr := container.ID()
if innerErr != nil {
log.Error(innerErr)
}
stoppedContainers = append(stoppedContainers, containerID)
}
}
return nil
})
}), nil)
if err != nil {
return nil, err
}
return stoppedContainers, nil
}
// Delete removes the specified services (like docker rm).
func (p *Project) Delete(ctx context.Context, options options.Delete, services ...string) error {
stoppedContainers, err := p.listStoppedContainers(ctx, services...)
if err != nil {
return err
}
if len(stoppedContainers) == 0 {
p.Notify(events.ProjectDeleteDone, "", nil)
fmt.Println("No stopped containers")
return nil
}
if options.BeforeDeleteCallback != nil && !options.BeforeDeleteCallback(stoppedContainers) {
return nil
}
return p.perform(events.ProjectDeleteStart, events.ProjectDeleteDone, services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) {
wrapper.Do(nil, events.ServiceDeleteStart, events.ServiceDelete, func(service Service) error {
return service.Delete(ctx, options)
})
}), nil)
}
// Kill kills the specified services (like docker kill).
func (p *Project) Kill(ctx context.Context, signal string, services ...string) error {
return p.perform(events.ProjectKillStart, events.ProjectKillDone, services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) {
wrapper.Do(nil, events.ServiceKillStart, events.ServiceKill, func(service Service) error {
return service.Kill(ctx, signal)
})
}), nil)
}
// Pause pauses the specified services containers (like docker pause).
func (p *Project) Pause(ctx context.Context, services ...string) error {
return p.perform(events.ProjectPauseStart, events.ProjectPauseDone, services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) {
wrapper.Do(nil, events.ServicePauseStart, events.ServicePause, func(service Service) error {
return service.Pause(ctx)
})
}), nil)
}
// Unpause pauses the specified services containers (like docker pause).
func (p *Project) Unpause(ctx context.Context, services ...string) error {
return p.perform(events.ProjectUnpauseStart, events.ProjectUnpauseDone, services, wrapperAction(func(wrapper *serviceWrapper, wrappers map[string]*serviceWrapper) {
wrapper.Do(nil, events.ServiceUnpauseStart, events.ServiceUnpause, func(service Service) error {
return service.Unpause(ctx)
})
}), nil)
}
func (p *Project) perform(start, done events.EventType, services []string, action wrapperAction, cycleAction serviceAction) error {
p.Notify(start, "", nil)
err := p.forEach(services, action, cycleAction)
p.Notify(done, "", nil)
return err
}
func isSelected(wrapper *serviceWrapper, selected map[string]bool) bool {
return len(selected) == 0 || selected[wrapper.name]
}
func (p *Project) forEach(services []string, action wrapperAction, cycleAction serviceAction) error {
selected := make(map[string]bool)
wrappers := make(map[string]*serviceWrapper)
for _, s := range services {
selected[s] = true
}
return p.traverse(true, selected, wrappers, action, cycleAction)
}
func (p *Project) startService(wrappers map[string]*serviceWrapper, history []string, selected, launched map[string]bool, wrapper *serviceWrapper, action wrapperAction, cycleAction serviceAction) error {
if launched[wrapper.name] {
return nil
}
launched[wrapper.name] = true
history = append(history, wrapper.name)
for _, dep := range wrapper.service.DependentServices() {
target := wrappers[dep.Target]
if target == nil {
log.Debugf("Failed to find %s", dep.Target)
return fmt.Errorf("Service '%s' has a link to service '%s' which is undefined", wrapper.name, dep.Target)
}
if utils.Contains(history, dep.Target) {
cycle := strings.Join(append(history, dep.Target), "->")
if dep.Optional {
log.Debugf("Ignoring cycle for %s", cycle)
wrapper.IgnoreDep(dep.Target)
if cycleAction != nil {
var err error
log.Debugf("Running cycle action for %s", cycle)
err = cycleAction(target.service)
if err != nil {
return err
}
}
} else {
return fmt.Errorf("Cycle detected in path %s", cycle)
}
continue
}
err := p.startService(wrappers, history, selected, launched, target, action, cycleAction)
if err != nil {
return err
}
}
if isSelected(wrapper, selected) {
log.Debugf("Launching action for %s", wrapper.name)
go action(wrapper, wrappers)
} else {
wrapper.Ignore()
}
return nil
}
func (p *Project) traverse(start bool, selected map[string]bool, wrappers map[string]*serviceWrapper, action wrapperAction, cycleAction serviceAction) error {
restart := false
wrapperList := []string{}
if start {
for _, name := range p.ServiceConfigs.Keys() {
wrapperList = append(wrapperList, name)
}
} else {
for _, wrapper := range wrappers {
if err := wrapper.Reset(); err != nil {
return err
}
}
wrapperList = p.reload
}
p.loadWrappers(wrappers, wrapperList)
p.reload = []string{}
// check service name
for s := range selected {
if wrappers[s] == nil {
return errors.New("No such service: " + s)
}
}
launched := map[string]bool{}
for _, wrapper := range wrappers {
if err := p.startService(wrappers, []string{}, selected, launched, wrapper, action, cycleAction); err != nil {
return err
}
}
var firstError error
for _, wrapper := range wrappers {
if !isSelected(wrapper, selected) {
continue
}
if err := wrapper.Wait(); err == ErrRestart {
restart = true
} else if err != nil {
log.Errorf("Failed to start: %s : %v", wrapper.name, err)
if firstError == nil {
firstError = err
}
}
}
if restart {
if p.ReloadCallback != nil {
if err := p.ReloadCallback(); err != nil {
log.Errorf("Failed calling callback: %v", err)
}
}
return p.traverse(false, selected, wrappers, action, cycleAction)
}
return firstError
}
// AddListener adds the specified listener to the project.
// This implements implicitly events.Emitter.
func (p *Project) AddListener(c chan<- events.Event) {
if !p.hasListeners {
for _, l := range p.listeners {
close(l)
}
p.hasListeners = true
p.listeners = []chan<- events.Event{c}
} else {
p.listeners = append(p.listeners, c)
}
}
// Notify notifies all project listener with the specified eventType, service name and datas.
// This implements implicitly events.Notifier interface.
func (p *Project) Notify(eventType events.EventType, serviceName string, data map[string]string) {
if eventType == events.NoEvent {
return
}
event := events.Event{
EventType: eventType,
ServiceName: serviceName,
Data: data,
}
for _, l := range p.listeners {
l <- event
}
}

View file

@ -0,0 +1,24 @@
package project
import (
"golang.org/x/net/context"
"github.com/docker/libcompose/project/events"
)
// Events listen for real time events from containers (of the project).
func (p *Project) Events(ctx context.Context, services ...string) (chan events.ContainerEvent, error) {
events := make(chan events.ContainerEvent)
if len(services) == 0 {
services = p.ServiceConfigs.Keys()
}
// FIXME(vdemeester) handle errors (chan) here
for _, service := range services {
s, err := p.CreateService(service)
if err != nil {
return nil, err
}
go s.Events(ctx, events)
}
return events, nil
}

View file

@ -0,0 +1,115 @@
package project
import (
"sync"
log "github.com/Sirupsen/logrus"
"github.com/docker/libcompose/project/events"
)
type serviceWrapper struct {
name string
service Service
done sync.WaitGroup
state ServiceState
err error
project *Project
noWait bool
ignored map[string]bool
}
func newServiceWrapper(name string, p *Project) (*serviceWrapper, error) {
wrapper := &serviceWrapper{
name: name,
state: StateUnknown,
project: p,
ignored: map[string]bool{},
}
return wrapper, wrapper.Reset()
}
func (s *serviceWrapper) IgnoreDep(name string) {
s.ignored[name] = true
}
func (s *serviceWrapper) Reset() error {
if s.state != StateExecuted {
service, err := s.project.CreateService(s.name)
if err != nil {
log.Errorf("Failed to create service for %s : %v", s.name, err)
return err
}
s.service = service
}
if s.err == ErrRestart {
s.err = nil
}
s.done.Add(1)
return nil
}
func (s *serviceWrapper) Ignore() {
defer s.done.Done()
s.state = StateExecuted
s.project.Notify(events.ServiceUpIgnored, s.service.Name(), nil)
}
func (s *serviceWrapper) waitForDeps(wrappers map[string]*serviceWrapper) bool {
if s.noWait {
return true
}
for _, dep := range s.service.DependentServices() {
if s.ignored[dep.Target] {
continue
}
if wrapper, ok := wrappers[dep.Target]; ok {
if wrapper.Wait() == ErrRestart {
s.project.Notify(events.ProjectReload, wrapper.service.Name(), nil)
s.err = ErrRestart
return false
}
} else {
log.Errorf("Failed to find %s", dep.Target)
}
}
return true
}
func (s *serviceWrapper) Do(wrappers map[string]*serviceWrapper, start, done events.EventType, action func(service Service) error) {
defer s.done.Done()
if s.state == StateExecuted {
return
}
if wrappers != nil && !s.waitForDeps(wrappers) {
return
}
s.state = StateExecuted
s.project.Notify(start, s.service.Name(), nil)
s.err = action(s.service)
if s.err == ErrRestart {
s.project.Notify(done, s.service.Name(), nil)
s.project.Notify(events.ProjectReloadTrigger, s.service.Name(), nil)
} else if s.err != nil {
log.Errorf("Failed %s %s : %v", start, s.name, s.err)
} else {
s.project.Notify(done, s.service.Name(), nil)
}
}
func (s *serviceWrapper) Wait() error {
s.done.Wait()
return s.err
}

View file

@ -0,0 +1,97 @@
package project
import (
"errors"
"golang.org/x/net/context"
"github.com/docker/libcompose/config"
"github.com/docker/libcompose/project/events"
"github.com/docker/libcompose/project/options"
)
// Service defines what a libcompose service provides.
type Service interface {
Build(ctx context.Context, buildOptions options.Build) error
Create(ctx context.Context, options options.Create) error
Delete(ctx context.Context, options options.Delete) error
Events(ctx context.Context, messages chan events.ContainerEvent) error
Info(ctx context.Context, qFlag bool) (InfoSet, error)
Log(ctx context.Context, follow bool) error
Kill(ctx context.Context, signal string) error
Pause(ctx context.Context) error
Pull(ctx context.Context) error
Restart(ctx context.Context, timeout int) error
Run(ctx context.Context, commandParts []string, options options.Run) (int, error)
Scale(ctx context.Context, count int, timeout int) error
Start(ctx context.Context) error
Stop(ctx context.Context, timeout int) error
Unpause(ctx context.Context) error
Up(ctx context.Context, options options.Up) error
RemoveImage(ctx context.Context, imageType options.ImageType) error
Containers(ctx context.Context) ([]Container, error)
DependentServices() []ServiceRelationship
Config() *config.ServiceConfig
Name() string
}
// ServiceState holds the state of a service.
type ServiceState string
// State definitions
var (
StateExecuted = ServiceState("executed")
StateUnknown = ServiceState("unknown")
)
// Error definitions
var (
ErrRestart = errors.New("Restart execution")
ErrUnsupported = errors.New("UnsupportedOperation")
)
// ServiceFactory is an interface factory to create Service object for the specified
// project, with the specified name and service configuration.
type ServiceFactory interface {
Create(project *Project, name string, serviceConfig *config.ServiceConfig) (Service, error)
}
// ServiceRelationshipType defines the type of service relationship.
type ServiceRelationshipType string
// RelTypeLink means the services are linked (docker links).
const RelTypeLink = ServiceRelationshipType("")
// RelTypeNetNamespace means the services share the same network namespace.
const RelTypeNetNamespace = ServiceRelationshipType("netns")
// RelTypeIpcNamespace means the service share the same ipc namespace.
const RelTypeIpcNamespace = ServiceRelationshipType("ipc")
// RelTypeVolumesFrom means the services share some volumes.
const RelTypeVolumesFrom = ServiceRelationshipType("volumesFrom")
// RelTypeDependsOn means the dependency was explicitly set using 'depends_on'.
const RelTypeDependsOn = ServiceRelationshipType("dependsOn")
// RelTypeNetworkMode means the services depends on another service on networkMode
const RelTypeNetworkMode = ServiceRelationshipType("networkMode")
// ServiceRelationship holds the relationship information between two services.
type ServiceRelationship struct {
Target, Alias string
Type ServiceRelationshipType
Optional bool
}
// NewServiceRelationship creates a new Relationship based on the specified alias
// and relationship type.
func NewServiceRelationship(nameAlias string, relType ServiceRelationshipType) ServiceRelationship {
name, alias := NameAlias(nameAlias)
return ServiceRelationship{
Target: name,
Alias: alias,
Type: relType,
}
}

View file

@ -0,0 +1,47 @@
package project
import (
"strings"
)
// DefaultDependentServices return the dependent services (as an array of ServiceRelationship)
// for the specified project and service. It looks for : links, volumesFrom, net and ipc configuration.
func DefaultDependentServices(p *Project, s Service) []ServiceRelationship {
config := s.Config()
if config == nil {
return []ServiceRelationship{}
}
result := []ServiceRelationship{}
for _, link := range config.Links {
result = append(result, NewServiceRelationship(link, RelTypeLink))
}
for _, volumesFrom := range config.VolumesFrom {
result = append(result, NewServiceRelationship(volumesFrom, RelTypeVolumesFrom))
}
for _, dependsOn := range config.DependsOn {
result = append(result, NewServiceRelationship(dependsOn, RelTypeDependsOn))
}
if config.NetworkMode != "" {
if strings.HasPrefix(config.NetworkMode, "service:") {
serviceName := config.NetworkMode[8:]
result = append(result, NewServiceRelationship(serviceName, RelTypeNetworkMode))
}
}
return result
}
// NameAlias returns the name and alias based on the specified string.
// If the name contains a colon (like name:alias) it will split it, otherwise
// it will return the specified name as name and alias.
func NameAlias(name string) (string, string) {
parts := strings.SplitN(name, ":", 2)
if len(parts) == 2 {
return parts[0], parts[1]
}
return parts[0], parts[0]
}

View file

@ -0,0 +1,137 @@
package utils
import (
"encoding/json"
"sync"
"github.com/Sirupsen/logrus"
yaml "github.com/cloudfoundry-incubator/candiedyaml"
)
// InParallel holds a pool and a waitgroup to execute tasks in parallel and to be able
// to wait for completion of all tasks.
type InParallel struct {
wg sync.WaitGroup
pool sync.Pool
}
// Add runs the specified task in parallel and adds it to the waitGroup.
func (i *InParallel) Add(task func() error) {
i.wg.Add(1)
go func() {
defer i.wg.Done()
err := task()
if err != nil {
i.pool.Put(err)
}
}()
}
// Wait waits for all tasks to complete and returns the latest error encountered if any.
func (i *InParallel) Wait() error {
i.wg.Wait()
obj := i.pool.Get()
if err, ok := obj.(error); ok {
return err
}
return nil
}
// ConvertByJSON converts a struct (src) to another one (target) using json marshalling/unmarshalling.
// If the structure are not compatible, this will throw an error as the unmarshalling will fail.
func ConvertByJSON(src, target interface{}) error {
newBytes, err := json.Marshal(src)
if err != nil {
return err
}
err = json.Unmarshal(newBytes, target)
if err != nil {
logrus.Errorf("Failed to unmarshall: %v\n%s", err, string(newBytes))
}
return err
}
// Convert converts a struct (src) to another one (target) using yaml marshalling/unmarshalling.
// If the structure are not compatible, this will throw an error as the unmarshalling will fail.
func Convert(src, target interface{}) error {
newBytes, err := yaml.Marshal(src)
if err != nil {
return err
}
err = yaml.Unmarshal(newBytes, target)
if err != nil {
logrus.Errorf("Failed to unmarshall: %v\n%s", err, string(newBytes))
}
return err
}
// CopySlice creates an exact copy of the provided string slice
func CopySlice(s []string) []string {
if s == nil {
return nil
}
r := make([]string, len(s))
copy(r, s)
return r
}
// CopyMap creates an exact copy of the provided string-to-string map
func CopyMap(m map[string]string) map[string]string {
if m == nil {
return nil
}
r := map[string]string{}
for k, v := range m {
r[k] = v
}
return r
}
// FilterStringSet accepts a string set `s` (in the form of `map[string]bool`) and a filtering function `f`
// and returns a string set containing only the strings `x` for which `f(x) == true`
func FilterStringSet(s map[string]bool, f func(x string) bool) map[string]bool {
result := map[string]bool{}
for k := range s {
if f(k) {
result[k] = true
}
}
return result
}
// FilterString returns a json representation of the specified map
// that is used as filter for docker.
func FilterString(data map[string][]string) string {
// I can't imagine this would ever fail
bytes, _ := json.Marshal(data)
return string(bytes)
}
// Contains checks if the specified string (key) is present in the specified collection.
func Contains(collection []string, key string) bool {
for _, value := range collection {
if value == key {
return true
}
}
return false
}
// Merge performs a union of two string slices: the result is an unordered slice
// that includes every item from either argument exactly once
func Merge(coll1, coll2 []string) []string {
m := map[string]struct{}{}
for _, v := range append(coll1, coll2...) {
m[v] = struct{}{}
}
r := make([]string, 0, len(m))
for k := range m {
r = append(r, k)
}
return r
}

View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015 Microsoft Corporation
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View file

@ -0,0 +1,188 @@
package ansiterm
const LogEnv = "DEBUG_TERMINAL"
// ANSI constants
// References:
// -- http://www.ecma-international.org/publications/standards/Ecma-048.htm
// -- http://man7.org/linux/man-pages/man4/console_codes.4.html
// -- http://manpages.ubuntu.com/manpages/intrepid/man4/console_codes.4.html
// -- http://en.wikipedia.org/wiki/ANSI_escape_code
// -- http://vt100.net/emu/dec_ansi_parser
// -- http://vt100.net/emu/vt500_parser.svg
// -- http://invisible-island.net/xterm/ctlseqs/ctlseqs.html
// -- http://www.inwap.com/pdp10/ansicode.txt
const (
// ECMA-48 Set Graphics Rendition
// Note:
// -- Constants leading with an underscore (e.g., _ANSI_xxx) are unsupported or reserved
// -- Fonts could possibly be supported via SetCurrentConsoleFontEx
// -- Windows does not expose the per-window cursor (i.e., caret) blink times
ANSI_SGR_RESET = 0
ANSI_SGR_BOLD = 1
ANSI_SGR_DIM = 2
_ANSI_SGR_ITALIC = 3
ANSI_SGR_UNDERLINE = 4
_ANSI_SGR_BLINKSLOW = 5
_ANSI_SGR_BLINKFAST = 6
ANSI_SGR_REVERSE = 7
_ANSI_SGR_INVISIBLE = 8
_ANSI_SGR_LINETHROUGH = 9
_ANSI_SGR_FONT_00 = 10
_ANSI_SGR_FONT_01 = 11
_ANSI_SGR_FONT_02 = 12
_ANSI_SGR_FONT_03 = 13
_ANSI_SGR_FONT_04 = 14
_ANSI_SGR_FONT_05 = 15
_ANSI_SGR_FONT_06 = 16
_ANSI_SGR_FONT_07 = 17
_ANSI_SGR_FONT_08 = 18
_ANSI_SGR_FONT_09 = 19
_ANSI_SGR_FONT_10 = 20
_ANSI_SGR_DOUBLEUNDERLINE = 21
ANSI_SGR_BOLD_DIM_OFF = 22
_ANSI_SGR_ITALIC_OFF = 23
ANSI_SGR_UNDERLINE_OFF = 24
_ANSI_SGR_BLINK_OFF = 25
_ANSI_SGR_RESERVED_00 = 26
ANSI_SGR_REVERSE_OFF = 27
_ANSI_SGR_INVISIBLE_OFF = 28
_ANSI_SGR_LINETHROUGH_OFF = 29
ANSI_SGR_FOREGROUND_BLACK = 30
ANSI_SGR_FOREGROUND_RED = 31
ANSI_SGR_FOREGROUND_GREEN = 32
ANSI_SGR_FOREGROUND_YELLOW = 33
ANSI_SGR_FOREGROUND_BLUE = 34
ANSI_SGR_FOREGROUND_MAGENTA = 35
ANSI_SGR_FOREGROUND_CYAN = 36
ANSI_SGR_FOREGROUND_WHITE = 37
_ANSI_SGR_RESERVED_01 = 38
ANSI_SGR_FOREGROUND_DEFAULT = 39
ANSI_SGR_BACKGROUND_BLACK = 40
ANSI_SGR_BACKGROUND_RED = 41
ANSI_SGR_BACKGROUND_GREEN = 42
ANSI_SGR_BACKGROUND_YELLOW = 43
ANSI_SGR_BACKGROUND_BLUE = 44
ANSI_SGR_BACKGROUND_MAGENTA = 45
ANSI_SGR_BACKGROUND_CYAN = 46
ANSI_SGR_BACKGROUND_WHITE = 47
_ANSI_SGR_RESERVED_02 = 48
ANSI_SGR_BACKGROUND_DEFAULT = 49
// 50 - 65: Unsupported
ANSI_MAX_CMD_LENGTH = 4096
MAX_INPUT_EVENTS = 128
DEFAULT_WIDTH = 80
DEFAULT_HEIGHT = 24
ANSI_BEL = 0x07
ANSI_BACKSPACE = 0x08
ANSI_TAB = 0x09
ANSI_LINE_FEED = 0x0A
ANSI_VERTICAL_TAB = 0x0B
ANSI_FORM_FEED = 0x0C
ANSI_CARRIAGE_RETURN = 0x0D
ANSI_ESCAPE_PRIMARY = 0x1B
ANSI_ESCAPE_SECONDARY = 0x5B
ANSI_OSC_STRING_ENTRY = 0x5D
ANSI_COMMAND_FIRST = 0x40
ANSI_COMMAND_LAST = 0x7E
DCS_ENTRY = 0x90
CSI_ENTRY = 0x9B
OSC_STRING = 0x9D
ANSI_PARAMETER_SEP = ";"
ANSI_CMD_G0 = '('
ANSI_CMD_G1 = ')'
ANSI_CMD_G2 = '*'
ANSI_CMD_G3 = '+'
ANSI_CMD_DECPNM = '>'
ANSI_CMD_DECPAM = '='
ANSI_CMD_OSC = ']'
ANSI_CMD_STR_TERM = '\\'
KEY_CONTROL_PARAM_2 = ";2"
KEY_CONTROL_PARAM_3 = ";3"
KEY_CONTROL_PARAM_4 = ";4"
KEY_CONTROL_PARAM_5 = ";5"
KEY_CONTROL_PARAM_6 = ";6"
KEY_CONTROL_PARAM_7 = ";7"
KEY_CONTROL_PARAM_8 = ";8"
KEY_ESC_CSI = "\x1B["
KEY_ESC_N = "\x1BN"
KEY_ESC_O = "\x1BO"
FILL_CHARACTER = ' '
)
func getByteRange(start byte, end byte) []byte {
bytes := make([]byte, 0, 32)
for i := start; i <= end; i++ {
bytes = append(bytes, byte(i))
}
return bytes
}
var toGroundBytes = getToGroundBytes()
var executors = getExecuteBytes()
// SPACE 20+A0 hex Always and everywhere a blank space
// Intermediate 20-2F hex !"#$%&'()*+,-./
var intermeds = getByteRange(0x20, 0x2F)
// Parameters 30-3F hex 0123456789:;<=>?
// CSI Parameters 30-39, 3B hex 0123456789;
var csiParams = getByteRange(0x30, 0x3F)
var csiCollectables = append(getByteRange(0x30, 0x39), getByteRange(0x3B, 0x3F)...)
// Uppercase 40-5F hex @ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_
var upperCase = getByteRange(0x40, 0x5F)
// Lowercase 60-7E hex `abcdefghijlkmnopqrstuvwxyz{|}~
var lowerCase = getByteRange(0x60, 0x7E)
// Alphabetics 40-7E hex (all of upper and lower case)
var alphabetics = append(upperCase, lowerCase...)
var printables = getByteRange(0x20, 0x7F)
var escapeIntermediateToGroundBytes = getByteRange(0x30, 0x7E)
var escapeToGroundBytes = getEscapeToGroundBytes()
// See http://www.vt100.net/emu/vt500_parser.png for description of the complex
// byte ranges below
func getEscapeToGroundBytes() []byte {
escapeToGroundBytes := getByteRange(0x30, 0x4F)
escapeToGroundBytes = append(escapeToGroundBytes, getByteRange(0x51, 0x57)...)
escapeToGroundBytes = append(escapeToGroundBytes, 0x59)
escapeToGroundBytes = append(escapeToGroundBytes, 0x5A)
escapeToGroundBytes = append(escapeToGroundBytes, 0x5C)
escapeToGroundBytes = append(escapeToGroundBytes, getByteRange(0x60, 0x7E)...)
return escapeToGroundBytes
}
func getExecuteBytes() []byte {
executeBytes := getByteRange(0x00, 0x17)
executeBytes = append(executeBytes, 0x19)
executeBytes = append(executeBytes, getByteRange(0x1C, 0x1F)...)
return executeBytes
}
func getToGroundBytes() []byte {
groundBytes := []byte{0x18}
groundBytes = append(groundBytes, 0x1A)
groundBytes = append(groundBytes, getByteRange(0x80, 0x8F)...)
groundBytes = append(groundBytes, getByteRange(0x91, 0x97)...)
groundBytes = append(groundBytes, 0x99)
groundBytes = append(groundBytes, 0x9A)
groundBytes = append(groundBytes, 0x9C)
return groundBytes
}
// Delete 7F hex Always and everywhere ignored
// C1 Control 80-9F hex 32 additional control characters
// G1 Displayable A1-FE hex 94 additional displayable characters
// Special A0+FF hex Same as SPACE and DELETE

View file

@ -0,0 +1,7 @@
package ansiterm
type ansiContext struct {
currentChar byte
paramBuffer []byte
interBuffer []byte
}

View file

@ -0,0 +1,49 @@
package ansiterm
type csiEntryState struct {
baseState
}
func (csiState csiEntryState) Handle(b byte) (s state, e error) {
logger.Infof("CsiEntry::Handle %#x", b)
nextState, err := csiState.baseState.Handle(b)
if nextState != nil || err != nil {
return nextState, err
}
switch {
case sliceContains(alphabetics, b):
return csiState.parser.ground, nil
case sliceContains(csiCollectables, b):
return csiState.parser.csiParam, nil
case sliceContains(executors, b):
return csiState, csiState.parser.execute()
}
return csiState, nil
}
func (csiState csiEntryState) Transition(s state) error {
logger.Infof("CsiEntry::Transition %s --> %s", csiState.Name(), s.Name())
csiState.baseState.Transition(s)
switch s {
case csiState.parser.ground:
return csiState.parser.csiDispatch()
case csiState.parser.csiParam:
switch {
case sliceContains(csiParams, csiState.parser.context.currentChar):
csiState.parser.collectParam()
case sliceContains(intermeds, csiState.parser.context.currentChar):
csiState.parser.collectInter()
}
}
return nil
}
func (csiState csiEntryState) Enter() error {
csiState.parser.clear()
return nil
}

View file

@ -0,0 +1,38 @@
package ansiterm
type csiParamState struct {
baseState
}
func (csiState csiParamState) Handle(b byte) (s state, e error) {
logger.Infof("CsiParam::Handle %#x", b)
nextState, err := csiState.baseState.Handle(b)
if nextState != nil || err != nil {
return nextState, err
}
switch {
case sliceContains(alphabetics, b):
return csiState.parser.ground, nil
case sliceContains(csiCollectables, b):
csiState.parser.collectParam()
return csiState, nil
case sliceContains(executors, b):
return csiState, csiState.parser.execute()
}
return csiState, nil
}
func (csiState csiParamState) Transition(s state) error {
logger.Infof("CsiParam::Transition %s --> %s", csiState.Name(), s.Name())
csiState.baseState.Transition(s)
switch s {
case csiState.parser.ground:
return csiState.parser.csiDispatch()
}
return nil
}

View file

@ -0,0 +1,36 @@
package ansiterm
type escapeIntermediateState struct {
baseState
}
func (escState escapeIntermediateState) Handle(b byte) (s state, e error) {
logger.Infof("escapeIntermediateState::Handle %#x", b)
nextState, err := escState.baseState.Handle(b)
if nextState != nil || err != nil {
return nextState, err
}
switch {
case sliceContains(intermeds, b):
return escState, escState.parser.collectInter()
case sliceContains(executors, b):
return escState, escState.parser.execute()
case sliceContains(escapeIntermediateToGroundBytes, b):
return escState.parser.ground, nil
}
return escState, nil
}
func (escState escapeIntermediateState) Transition(s state) error {
logger.Infof("escapeIntermediateState::Transition %s --> %s", escState.Name(), s.Name())
escState.baseState.Transition(s)
switch s {
case escState.parser.ground:
return escState.parser.escDispatch()
}
return nil
}

View file

@ -0,0 +1,47 @@
package ansiterm
type escapeState struct {
baseState
}
func (escState escapeState) Handle(b byte) (s state, e error) {
logger.Infof("escapeState::Handle %#x", b)
nextState, err := escState.baseState.Handle(b)
if nextState != nil || err != nil {
return nextState, err
}
switch {
case b == ANSI_ESCAPE_SECONDARY:
return escState.parser.csiEntry, nil
case b == ANSI_OSC_STRING_ENTRY:
return escState.parser.oscString, nil
case sliceContains(executors, b):
return escState, escState.parser.execute()
case sliceContains(escapeToGroundBytes, b):
return escState.parser.ground, nil
case sliceContains(intermeds, b):
return escState.parser.escapeIntermediate, nil
}
return escState, nil
}
func (escState escapeState) Transition(s state) error {
logger.Infof("Escape::Transition %s --> %s", escState.Name(), s.Name())
escState.baseState.Transition(s)
switch s {
case escState.parser.ground:
return escState.parser.escDispatch()
case escState.parser.escapeIntermediate:
return escState.parser.collectInter()
}
return nil
}
func (escState escapeState) Enter() error {
escState.parser.clear()
return nil
}

View file

@ -0,0 +1,90 @@
package ansiterm
type AnsiEventHandler interface {
// Print
Print(b byte) error
// Execute C0 commands
Execute(b byte) error
// CUrsor Up
CUU(int) error
// CUrsor Down
CUD(int) error
// CUrsor Forward
CUF(int) error
// CUrsor Backward
CUB(int) error
// Cursor to Next Line
CNL(int) error
// Cursor to Previous Line
CPL(int) error
// Cursor Horizontal position Absolute
CHA(int) error
// Vertical line Position Absolute
VPA(int) error
// CUrsor Position
CUP(int, int) error
// Horizontal and Vertical Position (depends on PUM)
HVP(int, int) error
// Text Cursor Enable Mode
DECTCEM(bool) error
// Origin Mode
DECOM(bool) error
// 132 Column Mode
DECCOLM(bool) error
// Erase in Display
ED(int) error
// Erase in Line
EL(int) error
// Insert Line
IL(int) error
// Delete Line
DL(int) error
// Insert Character
ICH(int) error
// Delete Character
DCH(int) error
// Set Graphics Rendition
SGR([]int) error
// Pan Down
SU(int) error
// Pan Up
SD(int) error
// Device Attributes
DA([]string) error
// Set Top and Bottom Margins
DECSTBM(int, int) error
// Index
IND() error
// Reverse Index
RI() error
// Flush updates from previous commands
Flush() error
}

View file

@ -0,0 +1,24 @@
package ansiterm
type groundState struct {
baseState
}
func (gs groundState) Handle(b byte) (s state, e error) {
gs.parser.context.currentChar = b
nextState, err := gs.baseState.Handle(b)
if nextState != nil || err != nil {
return nextState, err
}
switch {
case sliceContains(printables, b):
return gs, gs.parser.print()
case sliceContains(executors, b):
return gs, gs.parser.execute()
}
return gs, nil
}

View file

@ -0,0 +1,31 @@
package ansiterm
type oscStringState struct {
baseState
}
func (oscState oscStringState) Handle(b byte) (s state, e error) {
logger.Infof("OscString::Handle %#x", b)
nextState, err := oscState.baseState.Handle(b)
if nextState != nil || err != nil {
return nextState, err
}
switch {
case isOscStringTerminator(b):
return oscState.parser.ground, nil
}
return oscState, nil
}
// See below for OSC string terminators for linux
// http://man7.org/linux/man-pages/man4/console_codes.4.html
func isOscStringTerminator(b byte) bool {
if b == ANSI_BEL || b == 0x5C {
return true
}
return false
}

View file

@ -0,0 +1,136 @@
package ansiterm
import (
"errors"
"io/ioutil"
"os"
"github.com/Sirupsen/logrus"
)
var logger *logrus.Logger
type AnsiParser struct {
currState state
eventHandler AnsiEventHandler
context *ansiContext
csiEntry state
csiParam state
dcsEntry state
escape state
escapeIntermediate state
error state
ground state
oscString state
stateMap []state
}
func CreateParser(initialState string, evtHandler AnsiEventHandler) *AnsiParser {
logFile := ioutil.Discard
if isDebugEnv := os.Getenv(LogEnv); isDebugEnv == "1" {
logFile, _ = os.Create("ansiParser.log")
}
logger = &logrus.Logger{
Out: logFile,
Formatter: new(logrus.TextFormatter),
Level: logrus.InfoLevel,
}
parser := &AnsiParser{
eventHandler: evtHandler,
context: &ansiContext{},
}
parser.csiEntry = csiEntryState{baseState{name: "CsiEntry", parser: parser}}
parser.csiParam = csiParamState{baseState{name: "CsiParam", parser: parser}}
parser.dcsEntry = dcsEntryState{baseState{name: "DcsEntry", parser: parser}}
parser.escape = escapeState{baseState{name: "Escape", parser: parser}}
parser.escapeIntermediate = escapeIntermediateState{baseState{name: "EscapeIntermediate", parser: parser}}
parser.error = errorState{baseState{name: "Error", parser: parser}}
parser.ground = groundState{baseState{name: "Ground", parser: parser}}
parser.oscString = oscStringState{baseState{name: "OscString", parser: parser}}
parser.stateMap = []state{
parser.csiEntry,
parser.csiParam,
parser.dcsEntry,
parser.escape,
parser.escapeIntermediate,
parser.error,
parser.ground,
parser.oscString,
}
parser.currState = getState(initialState, parser.stateMap)
logger.Infof("CreateParser: parser %p", parser)
return parser
}
func getState(name string, states []state) state {
for _, el := range states {
if el.Name() == name {
return el
}
}
return nil
}
func (ap *AnsiParser) Parse(bytes []byte) (int, error) {
for i, b := range bytes {
if err := ap.handle(b); err != nil {
return i, err
}
}
return len(bytes), ap.eventHandler.Flush()
}
func (ap *AnsiParser) handle(b byte) error {
ap.context.currentChar = b
newState, err := ap.currState.Handle(b)
if err != nil {
return err
}
if newState == nil {
logger.Warning("newState is nil")
return errors.New("New state of 'nil' is invalid.")
}
if newState != ap.currState {
if err := ap.changeState(newState); err != nil {
return err
}
}
return nil
}
func (ap *AnsiParser) changeState(newState state) error {
logger.Infof("ChangeState %s --> %s", ap.currState.Name(), newState.Name())
// Exit old state
if err := ap.currState.Exit(); err != nil {
logger.Infof("Exit state '%s' failed with : '%v'", ap.currState.Name(), err)
return err
}
// Perform transition action
if err := ap.currState.Transition(newState); err != nil {
logger.Infof("Transition from '%s' to '%s' failed with: '%v'", ap.currState.Name(), newState.Name, err)
return err
}
// Enter new state
if err := newState.Enter(); err != nil {
logger.Infof("Enter state '%s' failed with: '%v'", newState.Name(), err)
return err
}
ap.currState = newState
return nil
}

View file

@ -0,0 +1,103 @@
package ansiterm
import (
"strconv"
)
func parseParams(bytes []byte) ([]string, error) {
paramBuff := make([]byte, 0, 0)
params := []string{}
for _, v := range bytes {
if v == ';' {
if len(paramBuff) > 0 {
// Completed parameter, append it to the list
s := string(paramBuff)
params = append(params, s)
paramBuff = make([]byte, 0, 0)
}
} else {
paramBuff = append(paramBuff, v)
}
}
// Last parameter may not be terminated with ';'
if len(paramBuff) > 0 {
s := string(paramBuff)
params = append(params, s)
}
logger.Infof("Parsed params: %v with length: %d", params, len(params))
return params, nil
}
func parseCmd(context ansiContext) (string, error) {
return string(context.currentChar), nil
}
func getInt(params []string, dflt int) int {
i := getInts(params, 1, dflt)[0]
logger.Infof("getInt: %v", i)
return i
}
func getInts(params []string, minCount int, dflt int) []int {
ints := []int{}
for _, v := range params {
i, _ := strconv.Atoi(v)
// Zero is mapped to the default value in VT100.
if i == 0 {
i = dflt
}
ints = append(ints, i)
}
if len(ints) < minCount {
remaining := minCount - len(ints)
for i := 0; i < remaining; i++ {
ints = append(ints, dflt)
}
}
logger.Infof("getInts: %v", ints)
return ints
}
func (ap *AnsiParser) modeDispatch(param string, set bool) error {
switch param {
case "?3":
return ap.eventHandler.DECCOLM(set)
case "?6":
return ap.eventHandler.DECOM(set)
case "?25":
return ap.eventHandler.DECTCEM(set)
}
return nil
}
func (ap *AnsiParser) hDispatch(params []string) error {
if len(params) == 1 {
return ap.modeDispatch(params[0], true)
}
return nil
}
func (ap *AnsiParser) lDispatch(params []string) error {
if len(params) == 1 {
return ap.modeDispatch(params[0], false)
}
return nil
}
func getEraseParam(params []string) int {
param := getInt(params, 0)
if param < 0 || 3 < param {
param = 0
}
return param
}

View file

@ -0,0 +1,122 @@
package ansiterm
import (
"fmt"
)
func (ap *AnsiParser) collectParam() error {
currChar := ap.context.currentChar
logger.Infof("collectParam %#x", currChar)
ap.context.paramBuffer = append(ap.context.paramBuffer, currChar)
return nil
}
func (ap *AnsiParser) collectInter() error {
currChar := ap.context.currentChar
logger.Infof("collectInter %#x", currChar)
ap.context.paramBuffer = append(ap.context.interBuffer, currChar)
return nil
}
func (ap *AnsiParser) escDispatch() error {
cmd, _ := parseCmd(*ap.context)
intermeds := ap.context.interBuffer
logger.Infof("escDispatch currentChar: %#x", ap.context.currentChar)
logger.Infof("escDispatch: %v(%v)", cmd, intermeds)
switch cmd {
case "D": // IND
return ap.eventHandler.IND()
case "E": // NEL, equivalent to CRLF
err := ap.eventHandler.Execute(ANSI_CARRIAGE_RETURN)
if err == nil {
err = ap.eventHandler.Execute(ANSI_LINE_FEED)
}
return err
case "M": // RI
return ap.eventHandler.RI()
}
return nil
}
func (ap *AnsiParser) csiDispatch() error {
cmd, _ := parseCmd(*ap.context)
params, _ := parseParams(ap.context.paramBuffer)
logger.Infof("csiDispatch: %v(%v)", cmd, params)
switch cmd {
case "@":
return ap.eventHandler.ICH(getInt(params, 1))
case "A":
return ap.eventHandler.CUU(getInt(params, 1))
case "B":
return ap.eventHandler.CUD(getInt(params, 1))
case "C":
return ap.eventHandler.CUF(getInt(params, 1))
case "D":
return ap.eventHandler.CUB(getInt(params, 1))
case "E":
return ap.eventHandler.CNL(getInt(params, 1))
case "F":
return ap.eventHandler.CPL(getInt(params, 1))
case "G":
return ap.eventHandler.CHA(getInt(params, 1))
case "H":
ints := getInts(params, 2, 1)
x, y := ints[0], ints[1]
return ap.eventHandler.CUP(x, y)
case "J":
param := getEraseParam(params)
return ap.eventHandler.ED(param)
case "K":
param := getEraseParam(params)
return ap.eventHandler.EL(param)
case "L":
return ap.eventHandler.IL(getInt(params, 1))
case "M":
return ap.eventHandler.DL(getInt(params, 1))
case "P":
return ap.eventHandler.DCH(getInt(params, 1))
case "S":
return ap.eventHandler.SU(getInt(params, 1))
case "T":
return ap.eventHandler.SD(getInt(params, 1))
case "c":
return ap.eventHandler.DA(params)
case "d":
return ap.eventHandler.VPA(getInt(params, 1))
case "f":
ints := getInts(params, 2, 1)
x, y := ints[0], ints[1]
return ap.eventHandler.HVP(x, y)
case "h":
return ap.hDispatch(params)
case "l":
return ap.lDispatch(params)
case "m":
return ap.eventHandler.SGR(getInts(params, 1, 0))
case "r":
ints := getInts(params, 2, 1)
top, bottom := ints[0], ints[1]
return ap.eventHandler.DECSTBM(top, bottom)
default:
logger.Errorf(fmt.Sprintf("Unsupported CSI command: '%s', with full context: %v", cmd, ap.context))
return nil
}
}
func (ap *AnsiParser) print() error {
return ap.eventHandler.Print(ap.context.currentChar)
}
func (ap *AnsiParser) clear() error {
ap.context = &ansiContext{}
return nil
}
func (ap *AnsiParser) execute() error {
return ap.eventHandler.Execute(ap.context.currentChar)
}

View file

@ -0,0 +1,71 @@
package ansiterm
type stateID int
type state interface {
Enter() error
Exit() error
Handle(byte) (state, error)
Name() string
Transition(state) error
}
type baseState struct {
name string
parser *AnsiParser
}
func (base baseState) Enter() error {
return nil
}
func (base baseState) Exit() error {
return nil
}
func (base baseState) Handle(b byte) (s state, e error) {
switch {
case b == CSI_ENTRY:
return base.parser.csiEntry, nil
case b == DCS_ENTRY:
return base.parser.dcsEntry, nil
case b == ANSI_ESCAPE_PRIMARY:
return base.parser.escape, nil
case b == OSC_STRING:
return base.parser.oscString, nil
case sliceContains(toGroundBytes, b):
return base.parser.ground, nil
}
return nil, nil
}
func (base baseState) Name() string {
return base.name
}
func (base baseState) Transition(s state) error {
if s == base.parser.ground {
execBytes := []byte{0x18}
execBytes = append(execBytes, 0x1A)
execBytes = append(execBytes, getByteRange(0x80, 0x8F)...)
execBytes = append(execBytes, getByteRange(0x91, 0x97)...)
execBytes = append(execBytes, 0x99)
execBytes = append(execBytes, 0x9A)
if sliceContains(execBytes, base.parser.context.currentChar) {
return base.parser.execute()
}
}
return nil
}
type dcsEntryState struct {
baseState
}
type errorState struct {
baseState
}

View file

@ -0,0 +1,21 @@
package ansiterm
import (
"strconv"
)
func sliceContains(bytes []byte, b byte) bool {
for _, v := range bytes {
if v == b {
return true
}
}
return false
}
func convertBytesToInteger(bytes []byte) int {
s := string(bytes)
i, _ := strconv.Atoi(s)
return i
}

View file

@ -0,0 +1,182 @@
// +build windows
package winterm
import (
"fmt"
"os"
"strconv"
"strings"
"syscall"
"github.com/Azure/go-ansiterm"
)
// Windows keyboard constants
// See https://msdn.microsoft.com/en-us/library/windows/desktop/dd375731(v=vs.85).aspx.
const (
VK_PRIOR = 0x21 // PAGE UP key
VK_NEXT = 0x22 // PAGE DOWN key
VK_END = 0x23 // END key
VK_HOME = 0x24 // HOME key
VK_LEFT = 0x25 // LEFT ARROW key
VK_UP = 0x26 // UP ARROW key
VK_RIGHT = 0x27 // RIGHT ARROW key
VK_DOWN = 0x28 // DOWN ARROW key
VK_SELECT = 0x29 // SELECT key
VK_PRINT = 0x2A // PRINT key
VK_EXECUTE = 0x2B // EXECUTE key
VK_SNAPSHOT = 0x2C // PRINT SCREEN key
VK_INSERT = 0x2D // INS key
VK_DELETE = 0x2E // DEL key
VK_HELP = 0x2F // HELP key
VK_F1 = 0x70 // F1 key
VK_F2 = 0x71 // F2 key
VK_F3 = 0x72 // F3 key
VK_F4 = 0x73 // F4 key
VK_F5 = 0x74 // F5 key
VK_F6 = 0x75 // F6 key
VK_F7 = 0x76 // F7 key
VK_F8 = 0x77 // F8 key
VK_F9 = 0x78 // F9 key
VK_F10 = 0x79 // F10 key
VK_F11 = 0x7A // F11 key
VK_F12 = 0x7B // F12 key
RIGHT_ALT_PRESSED = 0x0001
LEFT_ALT_PRESSED = 0x0002
RIGHT_CTRL_PRESSED = 0x0004
LEFT_CTRL_PRESSED = 0x0008
SHIFT_PRESSED = 0x0010
NUMLOCK_ON = 0x0020
SCROLLLOCK_ON = 0x0040
CAPSLOCK_ON = 0x0080
ENHANCED_KEY = 0x0100
)
type ansiCommand struct {
CommandBytes []byte
Command string
Parameters []string
IsSpecial bool
}
func newAnsiCommand(command []byte) *ansiCommand {
if isCharacterSelectionCmdChar(command[1]) {
// Is Character Set Selection commands
return &ansiCommand{
CommandBytes: command,
Command: string(command),
IsSpecial: true,
}
}
// last char is command character
lastCharIndex := len(command) - 1
ac := &ansiCommand{
CommandBytes: command,
Command: string(command[lastCharIndex]),
IsSpecial: false,
}
// more than a single escape
if lastCharIndex != 0 {
start := 1
// skip if double char escape sequence
if command[0] == ansiterm.ANSI_ESCAPE_PRIMARY && command[1] == ansiterm.ANSI_ESCAPE_SECONDARY {
start++
}
// convert this to GetNextParam method
ac.Parameters = strings.Split(string(command[start:lastCharIndex]), ansiterm.ANSI_PARAMETER_SEP)
}
return ac
}
func (ac *ansiCommand) paramAsSHORT(index int, defaultValue int16) int16 {
if index < 0 || index >= len(ac.Parameters) {
return defaultValue
}
param, err := strconv.ParseInt(ac.Parameters[index], 10, 16)
if err != nil {
return defaultValue
}
return int16(param)
}
func (ac *ansiCommand) String() string {
return fmt.Sprintf("0x%v \"%v\" (\"%v\")",
bytesToHex(ac.CommandBytes),
ac.Command,
strings.Join(ac.Parameters, "\",\""))
}
// isAnsiCommandChar returns true if the passed byte falls within the range of ANSI commands.
// See http://manpages.ubuntu.com/manpages/intrepid/man4/console_codes.4.html.
func isAnsiCommandChar(b byte) bool {
switch {
case ansiterm.ANSI_COMMAND_FIRST <= b && b <= ansiterm.ANSI_COMMAND_LAST && b != ansiterm.ANSI_ESCAPE_SECONDARY:
return true
case b == ansiterm.ANSI_CMD_G1 || b == ansiterm.ANSI_CMD_OSC || b == ansiterm.ANSI_CMD_DECPAM || b == ansiterm.ANSI_CMD_DECPNM:
// non-CSI escape sequence terminator
return true
case b == ansiterm.ANSI_CMD_STR_TERM || b == ansiterm.ANSI_BEL:
// String escape sequence terminator
return true
}
return false
}
func isXtermOscSequence(command []byte, current byte) bool {
return (len(command) >= 2 && command[0] == ansiterm.ANSI_ESCAPE_PRIMARY && command[1] == ansiterm.ANSI_CMD_OSC && current != ansiterm.ANSI_BEL)
}
func isCharacterSelectionCmdChar(b byte) bool {
return (b == ansiterm.ANSI_CMD_G0 || b == ansiterm.ANSI_CMD_G1 || b == ansiterm.ANSI_CMD_G2 || b == ansiterm.ANSI_CMD_G3)
}
// bytesToHex converts a slice of bytes to a human-readable string.
func bytesToHex(b []byte) string {
hex := make([]string, len(b))
for i, ch := range b {
hex[i] = fmt.Sprintf("%X", ch)
}
return strings.Join(hex, "")
}
// ensureInRange adjusts the passed value, if necessary, to ensure it is within
// the passed min / max range.
func ensureInRange(n int16, min int16, max int16) int16 {
if n < min {
return min
} else if n > max {
return max
} else {
return n
}
}
func GetStdFile(nFile int) (*os.File, uintptr) {
var file *os.File
switch nFile {
case syscall.STD_INPUT_HANDLE:
file = os.Stdin
case syscall.STD_OUTPUT_HANDLE:
file = os.Stdout
case syscall.STD_ERROR_HANDLE:
file = os.Stderr
default:
panic(fmt.Errorf("Invalid standard handle identifier: %v", nFile))
}
fd, err := syscall.GetStdHandle(nFile)
if err != nil {
panic(fmt.Errorf("Invalid standard handle indentifier: %v -- %v", nFile, err))
}
return file, uintptr(fd)
}

View file

@ -0,0 +1,322 @@
// +build windows
package winterm
import (
"fmt"
"syscall"
"unsafe"
)
//===========================================================================================================
// IMPORTANT NOTE:
//
// The methods below make extensive use of the "unsafe" package to obtain the required pointers.
// Beginning in Go 1.3, the garbage collector may release local variables (e.g., incoming arguments, stack
// variables) the pointers reference *before* the API completes.
//
// As a result, in those cases, the code must hint that the variables remain in active by invoking the
// dummy method "use" (see below). Newer versions of Go are planned to change the mechanism to no longer
// require unsafe pointers.
//
// If you add or modify methods, ENSURE protection of local variables through the "use" builtin to inform
// the garbage collector the variables remain in use if:
//
// -- The value is not a pointer (e.g., int32, struct)
// -- The value is not referenced by the method after passing the pointer to Windows
//
// See http://golang.org/doc/go1.3.
//===========================================================================================================
var (
kernel32DLL = syscall.NewLazyDLL("kernel32.dll")
getConsoleCursorInfoProc = kernel32DLL.NewProc("GetConsoleCursorInfo")
setConsoleCursorInfoProc = kernel32DLL.NewProc("SetConsoleCursorInfo")
setConsoleCursorPositionProc = kernel32DLL.NewProc("SetConsoleCursorPosition")
setConsoleModeProc = kernel32DLL.NewProc("SetConsoleMode")
getConsoleScreenBufferInfoProc = kernel32DLL.NewProc("GetConsoleScreenBufferInfo")
setConsoleScreenBufferSizeProc = kernel32DLL.NewProc("SetConsoleScreenBufferSize")
scrollConsoleScreenBufferProc = kernel32DLL.NewProc("ScrollConsoleScreenBufferA")
setConsoleTextAttributeProc = kernel32DLL.NewProc("SetConsoleTextAttribute")
setConsoleWindowInfoProc = kernel32DLL.NewProc("SetConsoleWindowInfo")
writeConsoleOutputProc = kernel32DLL.NewProc("WriteConsoleOutputW")
readConsoleInputProc = kernel32DLL.NewProc("ReadConsoleInputW")
waitForSingleObjectProc = kernel32DLL.NewProc("WaitForSingleObject")
)
// Windows Console constants
const (
// Console modes
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms686033(v=vs.85).aspx.
ENABLE_PROCESSED_INPUT = 0x0001
ENABLE_LINE_INPUT = 0x0002
ENABLE_ECHO_INPUT = 0x0004
ENABLE_WINDOW_INPUT = 0x0008
ENABLE_MOUSE_INPUT = 0x0010
ENABLE_INSERT_MODE = 0x0020
ENABLE_QUICK_EDIT_MODE = 0x0040
ENABLE_EXTENDED_FLAGS = 0x0080
ENABLE_PROCESSED_OUTPUT = 0x0001
ENABLE_WRAP_AT_EOL_OUTPUT = 0x0002
// Character attributes
// Note:
// -- The attributes are combined to produce various colors (e.g., Blue + Green will create Cyan).
// Clearing all foreground or background colors results in black; setting all creates white.
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms682088(v=vs.85).aspx#_win32_character_attributes.
FOREGROUND_BLUE uint16 = 0x0001
FOREGROUND_GREEN uint16 = 0x0002
FOREGROUND_RED uint16 = 0x0004
FOREGROUND_INTENSITY uint16 = 0x0008
FOREGROUND_MASK uint16 = 0x000F
BACKGROUND_BLUE uint16 = 0x0010
BACKGROUND_GREEN uint16 = 0x0020
BACKGROUND_RED uint16 = 0x0040
BACKGROUND_INTENSITY uint16 = 0x0080
BACKGROUND_MASK uint16 = 0x00F0
COMMON_LVB_MASK uint16 = 0xFF00
COMMON_LVB_REVERSE_VIDEO uint16 = 0x4000
COMMON_LVB_UNDERSCORE uint16 = 0x8000
// Input event types
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms683499(v=vs.85).aspx.
KEY_EVENT = 0x0001
MOUSE_EVENT = 0x0002
WINDOW_BUFFER_SIZE_EVENT = 0x0004
MENU_EVENT = 0x0008
FOCUS_EVENT = 0x0010
// WaitForSingleObject return codes
WAIT_ABANDONED = 0x00000080
WAIT_FAILED = 0xFFFFFFFF
WAIT_SIGNALED = 0x0000000
WAIT_TIMEOUT = 0x00000102
// WaitForSingleObject wait duration
WAIT_INFINITE = 0xFFFFFFFF
WAIT_ONE_SECOND = 1000
WAIT_HALF_SECOND = 500
WAIT_QUARTER_SECOND = 250
)
// Windows API Console types
// -- See https://msdn.microsoft.com/en-us/library/windows/desktop/ms682101(v=vs.85).aspx for Console specific types (e.g., COORD)
// -- See https://msdn.microsoft.com/en-us/library/aa296569(v=vs.60).aspx for comments on alignment
type (
CHAR_INFO struct {
UnicodeChar uint16
Attributes uint16
}
CONSOLE_CURSOR_INFO struct {
Size uint32
Visible int32
}
CONSOLE_SCREEN_BUFFER_INFO struct {
Size COORD
CursorPosition COORD
Attributes uint16
Window SMALL_RECT
MaximumWindowSize COORD
}
COORD struct {
X int16
Y int16
}
SMALL_RECT struct {
Left int16
Top int16
Right int16
Bottom int16
}
// INPUT_RECORD is a C/C++ union of which KEY_EVENT_RECORD is one case, it is also the largest
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms683499(v=vs.85).aspx.
INPUT_RECORD struct {
EventType uint16
KeyEvent KEY_EVENT_RECORD
}
KEY_EVENT_RECORD struct {
KeyDown int32
RepeatCount uint16
VirtualKeyCode uint16
VirtualScanCode uint16
UnicodeChar uint16
ControlKeyState uint32
}
WINDOW_BUFFER_SIZE struct {
Size COORD
}
)
// boolToBOOL converts a Go bool into a Windows int32.
func boolToBOOL(f bool) int32 {
if f {
return int32(1)
} else {
return int32(0)
}
}
// GetConsoleCursorInfo retrieves information about the size and visiblity of the console cursor.
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms683163(v=vs.85).aspx.
func GetConsoleCursorInfo(handle uintptr, cursorInfo *CONSOLE_CURSOR_INFO) error {
r1, r2, err := getConsoleCursorInfoProc.Call(handle, uintptr(unsafe.Pointer(cursorInfo)), 0)
return checkError(r1, r2, err)
}
// SetConsoleCursorInfo sets the size and visiblity of the console cursor.
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms686019(v=vs.85).aspx.
func SetConsoleCursorInfo(handle uintptr, cursorInfo *CONSOLE_CURSOR_INFO) error {
r1, r2, err := setConsoleCursorInfoProc.Call(handle, uintptr(unsafe.Pointer(cursorInfo)), 0)
return checkError(r1, r2, err)
}
// SetConsoleCursorPosition location of the console cursor.
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms686025(v=vs.85).aspx.
func SetConsoleCursorPosition(handle uintptr, coord COORD) error {
r1, r2, err := setConsoleCursorPositionProc.Call(handle, coordToPointer(coord))
use(coord)
return checkError(r1, r2, err)
}
// GetConsoleMode gets the console mode for given file descriptor
// See http://msdn.microsoft.com/en-us/library/windows/desktop/ms683167(v=vs.85).aspx.
func GetConsoleMode(handle uintptr) (mode uint32, err error) {
err = syscall.GetConsoleMode(syscall.Handle(handle), &mode)
return mode, err
}
// SetConsoleMode sets the console mode for given file descriptor
// See http://msdn.microsoft.com/en-us/library/windows/desktop/ms686033(v=vs.85).aspx.
func SetConsoleMode(handle uintptr, mode uint32) error {
r1, r2, err := setConsoleModeProc.Call(handle, uintptr(mode), 0)
use(mode)
return checkError(r1, r2, err)
}
// GetConsoleScreenBufferInfo retrieves information about the specified console screen buffer.
// See http://msdn.microsoft.com/en-us/library/windows/desktop/ms683171(v=vs.85).aspx.
func GetConsoleScreenBufferInfo(handle uintptr) (*CONSOLE_SCREEN_BUFFER_INFO, error) {
info := CONSOLE_SCREEN_BUFFER_INFO{}
err := checkError(getConsoleScreenBufferInfoProc.Call(handle, uintptr(unsafe.Pointer(&info)), 0))
if err != nil {
return nil, err
}
return &info, nil
}
func ScrollConsoleScreenBuffer(handle uintptr, scrollRect SMALL_RECT, clipRect SMALL_RECT, destOrigin COORD, char CHAR_INFO) error {
r1, r2, err := scrollConsoleScreenBufferProc.Call(handle, uintptr(unsafe.Pointer(&scrollRect)), uintptr(unsafe.Pointer(&clipRect)), coordToPointer(destOrigin), uintptr(unsafe.Pointer(&char)))
use(scrollRect)
use(clipRect)
use(destOrigin)
use(char)
return checkError(r1, r2, err)
}
// SetConsoleScreenBufferSize sets the size of the console screen buffer.
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms686044(v=vs.85).aspx.
func SetConsoleScreenBufferSize(handle uintptr, coord COORD) error {
r1, r2, err := setConsoleScreenBufferSizeProc.Call(handle, coordToPointer(coord))
use(coord)
return checkError(r1, r2, err)
}
// SetConsoleTextAttribute sets the attributes of characters written to the
// console screen buffer by the WriteFile or WriteConsole function.
// See http://msdn.microsoft.com/en-us/library/windows/desktop/ms686047(v=vs.85).aspx.
func SetConsoleTextAttribute(handle uintptr, attribute uint16) error {
r1, r2, err := setConsoleTextAttributeProc.Call(handle, uintptr(attribute), 0)
use(attribute)
return checkError(r1, r2, err)
}
// SetConsoleWindowInfo sets the size and position of the console screen buffer's window.
// Note that the size and location must be within and no larger than the backing console screen buffer.
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms686125(v=vs.85).aspx.
func SetConsoleWindowInfo(handle uintptr, isAbsolute bool, rect SMALL_RECT) error {
r1, r2, err := setConsoleWindowInfoProc.Call(handle, uintptr(boolToBOOL(isAbsolute)), uintptr(unsafe.Pointer(&rect)))
use(isAbsolute)
use(rect)
return checkError(r1, r2, err)
}
// WriteConsoleOutput writes the CHAR_INFOs from the provided buffer to the active console buffer.
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms687404(v=vs.85).aspx.
func WriteConsoleOutput(handle uintptr, buffer []CHAR_INFO, bufferSize COORD, bufferCoord COORD, writeRegion *SMALL_RECT) error {
r1, r2, err := writeConsoleOutputProc.Call(handle, uintptr(unsafe.Pointer(&buffer[0])), coordToPointer(bufferSize), coordToPointer(bufferCoord), uintptr(unsafe.Pointer(writeRegion)))
use(buffer)
use(bufferSize)
use(bufferCoord)
return checkError(r1, r2, err)
}
// ReadConsoleInput reads (and removes) data from the console input buffer.
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms684961(v=vs.85).aspx.
func ReadConsoleInput(handle uintptr, buffer []INPUT_RECORD, count *uint32) error {
r1, r2, err := readConsoleInputProc.Call(handle, uintptr(unsafe.Pointer(&buffer[0])), uintptr(len(buffer)), uintptr(unsafe.Pointer(count)))
use(buffer)
return checkError(r1, r2, err)
}
// WaitForSingleObject waits for the passed handle to be signaled.
// It returns true if the handle was signaled; false otherwise.
// See https://msdn.microsoft.com/en-us/library/windows/desktop/ms687032(v=vs.85).aspx.
func WaitForSingleObject(handle uintptr, msWait uint32) (bool, error) {
r1, _, err := waitForSingleObjectProc.Call(handle, uintptr(uint32(msWait)))
switch r1 {
case WAIT_ABANDONED, WAIT_TIMEOUT:
return false, nil
case WAIT_SIGNALED:
return true, nil
}
use(msWait)
return false, err
}
// String helpers
func (info CONSOLE_SCREEN_BUFFER_INFO) String() string {
return fmt.Sprintf("Size(%v) Cursor(%v) Window(%v) Max(%v)", info.Size, info.CursorPosition, info.Window, info.MaximumWindowSize)
}
func (coord COORD) String() string {
return fmt.Sprintf("%v,%v", coord.X, coord.Y)
}
func (rect SMALL_RECT) String() string {
return fmt.Sprintf("(%v,%v),(%v,%v)", rect.Left, rect.Top, rect.Right, rect.Bottom)
}
// checkError evaluates the results of a Windows API call and returns the error if it failed.
func checkError(r1, r2 uintptr, err error) error {
// Windows APIs return non-zero to indicate success
if r1 != 0 {
return nil
}
// Return the error if provided, otherwise default to EINVAL
if err != nil {
return err
}
return syscall.EINVAL
}
// coordToPointer converts a COORD into a uintptr (by fooling the type system).
func coordToPointer(c COORD) uintptr {
// Note: This code assumes the two SHORTs are correctly laid out; the "cast" to uint32 is just to get a pointer to pass.
return uintptr(*((*uint32)(unsafe.Pointer(&c))))
}
// use is a no-op, but the compiler cannot see that it is.
// Calling use(p) ensures that p is kept live until that point.
func use(p interface{}) {}

View file

@ -0,0 +1,100 @@
// +build windows
package winterm
import "github.com/Azure/go-ansiterm"
const (
FOREGROUND_COLOR_MASK = FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE
BACKGROUND_COLOR_MASK = BACKGROUND_RED | BACKGROUND_GREEN | BACKGROUND_BLUE
)
// collectAnsiIntoWindowsAttributes modifies the passed Windows text mode flags to reflect the
// request represented by the passed ANSI mode.
func collectAnsiIntoWindowsAttributes(windowsMode uint16, inverted bool, baseMode uint16, ansiMode int16) (uint16, bool) {
switch ansiMode {
// Mode styles
case ansiterm.ANSI_SGR_BOLD:
windowsMode = windowsMode | FOREGROUND_INTENSITY
case ansiterm.ANSI_SGR_DIM, ansiterm.ANSI_SGR_BOLD_DIM_OFF:
windowsMode &^= FOREGROUND_INTENSITY
case ansiterm.ANSI_SGR_UNDERLINE:
windowsMode = windowsMode | COMMON_LVB_UNDERSCORE
case ansiterm.ANSI_SGR_REVERSE:
inverted = true
case ansiterm.ANSI_SGR_REVERSE_OFF:
inverted = false
case ansiterm.ANSI_SGR_UNDERLINE_OFF:
windowsMode &^= COMMON_LVB_UNDERSCORE
// Foreground colors
case ansiterm.ANSI_SGR_FOREGROUND_DEFAULT:
windowsMode = (windowsMode &^ FOREGROUND_MASK) | (baseMode & FOREGROUND_MASK)
case ansiterm.ANSI_SGR_FOREGROUND_BLACK:
windowsMode = (windowsMode &^ FOREGROUND_COLOR_MASK)
case ansiterm.ANSI_SGR_FOREGROUND_RED:
windowsMode = (windowsMode &^ FOREGROUND_COLOR_MASK) | FOREGROUND_RED
case ansiterm.ANSI_SGR_FOREGROUND_GREEN:
windowsMode = (windowsMode &^ FOREGROUND_COLOR_MASK) | FOREGROUND_GREEN
case ansiterm.ANSI_SGR_FOREGROUND_YELLOW:
windowsMode = (windowsMode &^ FOREGROUND_COLOR_MASK) | FOREGROUND_RED | FOREGROUND_GREEN
case ansiterm.ANSI_SGR_FOREGROUND_BLUE:
windowsMode = (windowsMode &^ FOREGROUND_COLOR_MASK) | FOREGROUND_BLUE
case ansiterm.ANSI_SGR_FOREGROUND_MAGENTA:
windowsMode = (windowsMode &^ FOREGROUND_COLOR_MASK) | FOREGROUND_RED | FOREGROUND_BLUE
case ansiterm.ANSI_SGR_FOREGROUND_CYAN:
windowsMode = (windowsMode &^ FOREGROUND_COLOR_MASK) | FOREGROUND_GREEN | FOREGROUND_BLUE
case ansiterm.ANSI_SGR_FOREGROUND_WHITE:
windowsMode = (windowsMode &^ FOREGROUND_COLOR_MASK) | FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE
// Background colors
case ansiterm.ANSI_SGR_BACKGROUND_DEFAULT:
// Black with no intensity
windowsMode = (windowsMode &^ BACKGROUND_MASK) | (baseMode & BACKGROUND_MASK)
case ansiterm.ANSI_SGR_BACKGROUND_BLACK:
windowsMode = (windowsMode &^ BACKGROUND_COLOR_MASK)
case ansiterm.ANSI_SGR_BACKGROUND_RED:
windowsMode = (windowsMode &^ BACKGROUND_COLOR_MASK) | BACKGROUND_RED
case ansiterm.ANSI_SGR_BACKGROUND_GREEN:
windowsMode = (windowsMode &^ BACKGROUND_COLOR_MASK) | BACKGROUND_GREEN
case ansiterm.ANSI_SGR_BACKGROUND_YELLOW:
windowsMode = (windowsMode &^ BACKGROUND_COLOR_MASK) | BACKGROUND_RED | BACKGROUND_GREEN
case ansiterm.ANSI_SGR_BACKGROUND_BLUE:
windowsMode = (windowsMode &^ BACKGROUND_COLOR_MASK) | BACKGROUND_BLUE
case ansiterm.ANSI_SGR_BACKGROUND_MAGENTA:
windowsMode = (windowsMode &^ BACKGROUND_COLOR_MASK) | BACKGROUND_RED | BACKGROUND_BLUE
case ansiterm.ANSI_SGR_BACKGROUND_CYAN:
windowsMode = (windowsMode &^ BACKGROUND_COLOR_MASK) | BACKGROUND_GREEN | BACKGROUND_BLUE
case ansiterm.ANSI_SGR_BACKGROUND_WHITE:
windowsMode = (windowsMode &^ BACKGROUND_COLOR_MASK) | BACKGROUND_RED | BACKGROUND_GREEN | BACKGROUND_BLUE
}
return windowsMode, inverted
}
// invertAttributes inverts the foreground and background colors of a Windows attributes value
func invertAttributes(windowsMode uint16) uint16 {
return (COMMON_LVB_MASK & windowsMode) | ((FOREGROUND_MASK & windowsMode) << 4) | ((BACKGROUND_MASK & windowsMode) >> 4)
}

View file

@ -0,0 +1,101 @@
// +build windows
package winterm
const (
horizontal = iota
vertical
)
func (h *windowsAnsiEventHandler) getCursorWindow(info *CONSOLE_SCREEN_BUFFER_INFO) SMALL_RECT {
if h.originMode {
sr := h.effectiveSr(info.Window)
return SMALL_RECT{
Top: sr.top,
Bottom: sr.bottom,
Left: 0,
Right: info.Size.X - 1,
}
} else {
return SMALL_RECT{
Top: info.Window.Top,
Bottom: info.Window.Bottom,
Left: 0,
Right: info.Size.X - 1,
}
}
}
// setCursorPosition sets the cursor to the specified position, bounded to the screen size
func (h *windowsAnsiEventHandler) setCursorPosition(position COORD, window SMALL_RECT) error {
position.X = ensureInRange(position.X, window.Left, window.Right)
position.Y = ensureInRange(position.Y, window.Top, window.Bottom)
err := SetConsoleCursorPosition(h.fd, position)
if err != nil {
return err
}
logger.Infof("Cursor position set: (%d, %d)", position.X, position.Y)
return err
}
func (h *windowsAnsiEventHandler) moveCursorVertical(param int) error {
return h.moveCursor(vertical, param)
}
func (h *windowsAnsiEventHandler) moveCursorHorizontal(param int) error {
return h.moveCursor(horizontal, param)
}
func (h *windowsAnsiEventHandler) moveCursor(moveMode int, param int) error {
info, err := GetConsoleScreenBufferInfo(h.fd)
if err != nil {
return err
}
position := info.CursorPosition
switch moveMode {
case horizontal:
position.X += int16(param)
case vertical:
position.Y += int16(param)
}
if err = h.setCursorPosition(position, h.getCursorWindow(info)); err != nil {
return err
}
return nil
}
func (h *windowsAnsiEventHandler) moveCursorLine(param int) error {
info, err := GetConsoleScreenBufferInfo(h.fd)
if err != nil {
return err
}
position := info.CursorPosition
position.X = 0
position.Y += int16(param)
if err = h.setCursorPosition(position, h.getCursorWindow(info)); err != nil {
return err
}
return nil
}
func (h *windowsAnsiEventHandler) moveCursorColumn(param int) error {
info, err := GetConsoleScreenBufferInfo(h.fd)
if err != nil {
return err
}
position := info.CursorPosition
position.X = int16(param) - 1
if err = h.setCursorPosition(position, h.getCursorWindow(info)); err != nil {
return err
}
return nil
}

View file

@ -0,0 +1,84 @@
// +build windows
package winterm
import "github.com/Azure/go-ansiterm"
func (h *windowsAnsiEventHandler) clearRange(attributes uint16, fromCoord COORD, toCoord COORD) error {
// Ignore an invalid (negative area) request
if toCoord.Y < fromCoord.Y {
return nil
}
var err error
var coordStart = COORD{}
var coordEnd = COORD{}
xCurrent, yCurrent := fromCoord.X, fromCoord.Y
xEnd, yEnd := toCoord.X, toCoord.Y
// Clear any partial initial line
if xCurrent > 0 {
coordStart.X, coordStart.Y = xCurrent, yCurrent
coordEnd.X, coordEnd.Y = xEnd, yCurrent
err = h.clearRect(attributes, coordStart, coordEnd)
if err != nil {
return err
}
xCurrent = 0
yCurrent += 1
}
// Clear intervening rectangular section
if yCurrent < yEnd {
coordStart.X, coordStart.Y = xCurrent, yCurrent
coordEnd.X, coordEnd.Y = xEnd, yEnd-1
err = h.clearRect(attributes, coordStart, coordEnd)
if err != nil {
return err
}
xCurrent = 0
yCurrent = yEnd
}
// Clear remaining partial ending line
coordStart.X, coordStart.Y = xCurrent, yCurrent
coordEnd.X, coordEnd.Y = xEnd, yEnd
err = h.clearRect(attributes, coordStart, coordEnd)
if err != nil {
return err
}
return nil
}
func (h *windowsAnsiEventHandler) clearRect(attributes uint16, fromCoord COORD, toCoord COORD) error {
region := SMALL_RECT{Top: fromCoord.Y, Left: fromCoord.X, Bottom: toCoord.Y, Right: toCoord.X}
width := toCoord.X - fromCoord.X + 1
height := toCoord.Y - fromCoord.Y + 1
size := uint32(width) * uint32(height)
if size <= 0 {
return nil
}
buffer := make([]CHAR_INFO, size)
char := CHAR_INFO{ansiterm.FILL_CHARACTER, attributes}
for i := 0; i < int(size); i++ {
buffer[i] = char
}
err := WriteConsoleOutput(h.fd, buffer, COORD{X: width, Y: height}, COORD{X: 0, Y: 0}, &region)
if err != nil {
return err
}
return nil
}

View file

@ -0,0 +1,118 @@
// +build windows
package winterm
// effectiveSr gets the current effective scroll region in buffer coordinates
func (h *windowsAnsiEventHandler) effectiveSr(window SMALL_RECT) scrollRegion {
top := addInRange(window.Top, h.sr.top, window.Top, window.Bottom)
bottom := addInRange(window.Top, h.sr.bottom, window.Top, window.Bottom)
if top >= bottom {
top = window.Top
bottom = window.Bottom
}
return scrollRegion{top: top, bottom: bottom}
}
func (h *windowsAnsiEventHandler) scrollUp(param int) error {
info, err := GetConsoleScreenBufferInfo(h.fd)
if err != nil {
return err
}
sr := h.effectiveSr(info.Window)
return h.scroll(param, sr, info)
}
func (h *windowsAnsiEventHandler) scrollDown(param int) error {
return h.scrollUp(-param)
}
func (h *windowsAnsiEventHandler) deleteLines(param int) error {
info, err := GetConsoleScreenBufferInfo(h.fd)
if err != nil {
return err
}
start := info.CursorPosition.Y
sr := h.effectiveSr(info.Window)
// Lines cannot be inserted or deleted outside the scrolling region.
if start >= sr.top && start <= sr.bottom {
sr.top = start
return h.scroll(param, sr, info)
} else {
return nil
}
}
func (h *windowsAnsiEventHandler) insertLines(param int) error {
return h.deleteLines(-param)
}
// scroll scrolls the provided scroll region by param lines. The scroll region is in buffer coordinates.
func (h *windowsAnsiEventHandler) scroll(param int, sr scrollRegion, info *CONSOLE_SCREEN_BUFFER_INFO) error {
logger.Infof("scroll: scrollTop: %d, scrollBottom: %d", sr.top, sr.bottom)
logger.Infof("scroll: windowTop: %d, windowBottom: %d", info.Window.Top, info.Window.Bottom)
// Copy from and clip to the scroll region (full buffer width)
scrollRect := SMALL_RECT{
Top: sr.top,
Bottom: sr.bottom,
Left: 0,
Right: info.Size.X - 1,
}
// Origin to which area should be copied
destOrigin := COORD{
X: 0,
Y: sr.top - int16(param),
}
char := CHAR_INFO{
UnicodeChar: ' ',
Attributes: h.attributes,
}
if err := ScrollConsoleScreenBuffer(h.fd, scrollRect, scrollRect, destOrigin, char); err != nil {
return err
}
return nil
}
func (h *windowsAnsiEventHandler) deleteCharacters(param int) error {
info, err := GetConsoleScreenBufferInfo(h.fd)
if err != nil {
return err
}
return h.scrollLine(param, info.CursorPosition, info)
}
func (h *windowsAnsiEventHandler) insertCharacters(param int) error {
return h.deleteCharacters(-param)
}
// scrollLine scrolls a line horizontally starting at the provided position by a number of columns.
func (h *windowsAnsiEventHandler) scrollLine(columns int, position COORD, info *CONSOLE_SCREEN_BUFFER_INFO) error {
// Copy from and clip to the scroll region (full buffer width)
scrollRect := SMALL_RECT{
Top: position.Y,
Bottom: position.Y,
Left: position.X,
Right: info.Size.X - 1,
}
// Origin to which area should be copied
destOrigin := COORD{
X: position.X - int16(columns),
Y: position.Y,
}
char := CHAR_INFO{
UnicodeChar: ' ',
Attributes: h.attributes,
}
if err := ScrollConsoleScreenBuffer(h.fd, scrollRect, scrollRect, destOrigin, char); err != nil {
return err
}
return nil
}

View file

@ -0,0 +1,9 @@
// +build windows
package winterm
// AddInRange increments a value by the passed quantity while ensuring the values
// always remain within the supplied min / max range.
func addInRange(n int16, increment int16, min int16, max int16) int16 {
return ensureInRange(n+increment, min, max)
}

View file

@ -0,0 +1,726 @@
// +build windows
package winterm
import (
"bytes"
"io/ioutil"
"os"
"strconv"
"github.com/Azure/go-ansiterm"
"github.com/Sirupsen/logrus"
)
var logger *logrus.Logger
type windowsAnsiEventHandler struct {
fd uintptr
file *os.File
infoReset *CONSOLE_SCREEN_BUFFER_INFO
sr scrollRegion
buffer bytes.Buffer
attributes uint16
inverted bool
wrapNext bool
drewMarginByte bool
originMode bool
marginByte byte
curInfo *CONSOLE_SCREEN_BUFFER_INFO
curPos COORD
}
func CreateWinEventHandler(fd uintptr, file *os.File) ansiterm.AnsiEventHandler {
logFile := ioutil.Discard
if isDebugEnv := os.Getenv(ansiterm.LogEnv); isDebugEnv == "1" {
logFile, _ = os.Create("winEventHandler.log")
}
logger = &logrus.Logger{
Out: logFile,
Formatter: new(logrus.TextFormatter),
Level: logrus.DebugLevel,
}
infoReset, err := GetConsoleScreenBufferInfo(fd)
if err != nil {
return nil
}
return &windowsAnsiEventHandler{
fd: fd,
file: file,
infoReset: infoReset,
attributes: infoReset.Attributes,
}
}
type scrollRegion struct {
top int16
bottom int16
}
// simulateLF simulates a LF or CR+LF by scrolling if necessary to handle the
// current cursor position and scroll region settings, in which case it returns
// true. If no special handling is necessary, then it does nothing and returns
// false.
//
// In the false case, the caller should ensure that a carriage return
// and line feed are inserted or that the text is otherwise wrapped.
func (h *windowsAnsiEventHandler) simulateLF(includeCR bool) (bool, error) {
if h.wrapNext {
if err := h.Flush(); err != nil {
return false, err
}
h.clearWrap()
}
pos, info, err := h.getCurrentInfo()
if err != nil {
return false, err
}
sr := h.effectiveSr(info.Window)
if pos.Y == sr.bottom {
// Scrolling is necessary. Let Windows automatically scroll if the scrolling region
// is the full window.
if sr.top == info.Window.Top && sr.bottom == info.Window.Bottom {
if includeCR {
pos.X = 0
h.updatePos(pos)
}
return false, nil
}
// A custom scroll region is active. Scroll the window manually to simulate
// the LF.
if err := h.Flush(); err != nil {
return false, err
}
logger.Info("Simulating LF inside scroll region")
if err := h.scrollUp(1); err != nil {
return false, err
}
if includeCR {
pos.X = 0
if err := SetConsoleCursorPosition(h.fd, pos); err != nil {
return false, err
}
}
return true, nil
} else if pos.Y < info.Window.Bottom {
// Let Windows handle the LF.
pos.Y++
if includeCR {
pos.X = 0
}
h.updatePos(pos)
return false, nil
} else {
// The cursor is at the bottom of the screen but outside the scroll
// region. Skip the LF.
logger.Info("Simulating LF outside scroll region")
if includeCR {
if err := h.Flush(); err != nil {
return false, err
}
pos.X = 0
if err := SetConsoleCursorPosition(h.fd, pos); err != nil {
return false, err
}
}
return true, nil
}
}
// executeLF executes a LF without a CR.
func (h *windowsAnsiEventHandler) executeLF() error {
handled, err := h.simulateLF(false)
if err != nil {
return err
}
if !handled {
// Windows LF will reset the cursor column position. Write the LF
// and restore the cursor position.
pos, _, err := h.getCurrentInfo()
if err != nil {
return err
}
h.buffer.WriteByte(ansiterm.ANSI_LINE_FEED)
if pos.X != 0 {
if err := h.Flush(); err != nil {
return err
}
logger.Info("Resetting cursor position for LF without CR")
if err := SetConsoleCursorPosition(h.fd, pos); err != nil {
return err
}
}
}
return nil
}
func (h *windowsAnsiEventHandler) Print(b byte) error {
if h.wrapNext {
h.buffer.WriteByte(h.marginByte)
h.clearWrap()
if _, err := h.simulateLF(true); err != nil {
return err
}
}
pos, info, err := h.getCurrentInfo()
if err != nil {
return err
}
if pos.X == info.Size.X-1 {
h.wrapNext = true
h.marginByte = b
} else {
pos.X++
h.updatePos(pos)
h.buffer.WriteByte(b)
}
return nil
}
func (h *windowsAnsiEventHandler) Execute(b byte) error {
switch b {
case ansiterm.ANSI_TAB:
logger.Info("Execute(TAB)")
// Move to the next tab stop, but preserve auto-wrap if already set.
if !h.wrapNext {
pos, info, err := h.getCurrentInfo()
if err != nil {
return err
}
pos.X = (pos.X + 8) - pos.X%8
if pos.X >= info.Size.X {
pos.X = info.Size.X - 1
}
if err := h.Flush(); err != nil {
return err
}
if err := SetConsoleCursorPosition(h.fd, pos); err != nil {
return err
}
}
return nil
case ansiterm.ANSI_BEL:
h.buffer.WriteByte(ansiterm.ANSI_BEL)
return nil
case ansiterm.ANSI_BACKSPACE:
if h.wrapNext {
if err := h.Flush(); err != nil {
return err
}
h.clearWrap()
}
pos, _, err := h.getCurrentInfo()
if err != nil {
return err
}
if pos.X > 0 {
pos.X--
h.updatePos(pos)
h.buffer.WriteByte(ansiterm.ANSI_BACKSPACE)
}
return nil
case ansiterm.ANSI_VERTICAL_TAB, ansiterm.ANSI_FORM_FEED:
// Treat as true LF.
return h.executeLF()
case ansiterm.ANSI_LINE_FEED:
// Simulate a CR and LF for now since there is no way in go-ansiterm
// to tell if the LF should include CR (and more things break when it's
// missing than when it's incorrectly added).
handled, err := h.simulateLF(true)
if handled || err != nil {
return err
}
return h.buffer.WriteByte(ansiterm.ANSI_LINE_FEED)
case ansiterm.ANSI_CARRIAGE_RETURN:
if h.wrapNext {
if err := h.Flush(); err != nil {
return err
}
h.clearWrap()
}
pos, _, err := h.getCurrentInfo()
if err != nil {
return err
}
if pos.X != 0 {
pos.X = 0
h.updatePos(pos)
h.buffer.WriteByte(ansiterm.ANSI_CARRIAGE_RETURN)
}
return nil
default:
return nil
}
}
func (h *windowsAnsiEventHandler) CUU(param int) error {
if err := h.Flush(); err != nil {
return err
}
logger.Infof("CUU: [%v]", []string{strconv.Itoa(param)})
h.clearWrap()
return h.moveCursorVertical(-param)
}
func (h *windowsAnsiEventHandler) CUD(param int) error {
if err := h.Flush(); err != nil {
return err
}
logger.Infof("CUD: [%v]", []string{strconv.Itoa(param)})
h.clearWrap()
return h.moveCursorVertical(param)
}
func (h *windowsAnsiEventHandler) CUF(param int) error {
if err := h.Flush(); err != nil {
return err
}
logger.Infof("CUF: [%v]", []string{strconv.Itoa(param)})
h.clearWrap()
return h.moveCursorHorizontal(param)
}
func (h *windowsAnsiEventHandler) CUB(param int) error {
if err := h.Flush(); err != nil {
return err
}
logger.Infof("CUB: [%v]", []string{strconv.Itoa(param)})
h.clearWrap()
return h.moveCursorHorizontal(-param)
}
func (h *windowsAnsiEventHandler) CNL(param int) error {
if err := h.Flush(); err != nil {
return err
}
logger.Infof("CNL: [%v]", []string{strconv.Itoa(param)})
h.clearWrap()
return h.moveCursorLine(param)
}
func (h *windowsAnsiEventHandler) CPL(param int) error {
if err := h.Flush(); err != nil {
return err
}
logger.Infof("CPL: [%v]", []string{strconv.Itoa(param)})
h.clearWrap()
return h.moveCursorLine(-param)
}
func (h *windowsAnsiEventHandler) CHA(param int) error {
if err := h.Flush(); err != nil {
return err
}
logger.Infof("CHA: [%v]", []string{strconv.Itoa(param)})
h.clearWrap()
return h.moveCursorColumn(param)
}
func (h *windowsAnsiEventHandler) VPA(param int) error {
if err := h.Flush(); err != nil {
return err
}
logger.Infof("VPA: [[%d]]", param)
h.clearWrap()
info, err := GetConsoleScreenBufferInfo(h.fd)
if err != nil {
return err
}
window := h.getCursorWindow(info)
position := info.CursorPosition
position.Y = window.Top + int16(param) - 1
return h.setCursorPosition(position, window)
}
func (h *windowsAnsiEventHandler) CUP(row int, col int) error {
if err := h.Flush(); err != nil {
return err
}
logger.Infof("CUP: [[%d %d]]", row, col)
h.clearWrap()
info, err := GetConsoleScreenBufferInfo(h.fd)
if err != nil {
return err
}
window := h.getCursorWindow(info)
position := COORD{window.Left + int16(col) - 1, window.Top + int16(row) - 1}
return h.setCursorPosition(position, window)
}
func (h *windowsAnsiEventHandler) HVP(row int, col int) error {
if err := h.Flush(); err != nil {
return err
}
logger.Infof("HVP: [[%d %d]]", row, col)
h.clearWrap()
return h.CUP(row, col)
}
func (h *windowsAnsiEventHandler) DECTCEM(visible bool) error {
if err := h.Flush(); err != nil {
return err
}
logger.Infof("DECTCEM: [%v]", []string{strconv.FormatBool(visible)})
h.clearWrap()
return nil
}
func (h *windowsAnsiEventHandler) DECOM(enable bool) error {
if err := h.Flush(); err != nil {
return err
}
logger.Infof("DECOM: [%v]", []string{strconv.FormatBool(enable)})
h.clearWrap()
h.originMode = enable
return h.CUP(1, 1)
}
func (h *windowsAnsiEventHandler) DECCOLM(use132 bool) error {
if err := h.Flush(); err != nil {
return err
}
logger.Infof("DECCOLM: [%v]", []string{strconv.FormatBool(use132)})
h.clearWrap()
if err := h.ED(2); err != nil {
return err
}
info, err := GetConsoleScreenBufferInfo(h.fd)
if err != nil {
return err
}
targetWidth := int16(80)
if use132 {
targetWidth = 132
}
if info.Size.X < targetWidth {
if err := SetConsoleScreenBufferSize(h.fd, COORD{targetWidth, info.Size.Y}); err != nil {
logger.Info("set buffer failed:", err)
return err
}
}
window := info.Window
window.Left = 0
window.Right = targetWidth - 1
if err := SetConsoleWindowInfo(h.fd, true, window); err != nil {
logger.Info("set window failed:", err)
return err
}
if info.Size.X > targetWidth {
if err := SetConsoleScreenBufferSize(h.fd, COORD{targetWidth, info.Size.Y}); err != nil {
logger.Info("set buffer failed:", err)
return err
}
}
return SetConsoleCursorPosition(h.fd, COORD{0, 0})
}
func (h *windowsAnsiEventHandler) ED(param int) error {
if err := h.Flush(); err != nil {
return err
}
logger.Infof("ED: [%v]", []string{strconv.Itoa(param)})
h.clearWrap()
// [J -- Erases from the cursor to the end of the screen, including the cursor position.
// [1J -- Erases from the beginning of the screen to the cursor, including the cursor position.
// [2J -- Erases the complete display. The cursor does not move.
// Notes:
// -- Clearing the entire buffer, versus just the Window, works best for Windows Consoles
info, err := GetConsoleScreenBufferInfo(h.fd)
if err != nil {
return err
}
var start COORD
var end COORD
switch param {
case 0:
start = info.CursorPosition
end = COORD{info.Size.X - 1, info.Size.Y - 1}
case 1:
start = COORD{0, 0}
end = info.CursorPosition
case 2:
start = COORD{0, 0}
end = COORD{info.Size.X - 1, info.Size.Y - 1}
}
err = h.clearRange(h.attributes, start, end)
if err != nil {
return err
}
// If the whole buffer was cleared, move the window to the top while preserving
// the window-relative cursor position.
if param == 2 {
pos := info.CursorPosition
window := info.Window
pos.Y -= window.Top
window.Bottom -= window.Top
window.Top = 0
if err := SetConsoleCursorPosition(h.fd, pos); err != nil {
return err
}
if err := SetConsoleWindowInfo(h.fd, true, window); err != nil {
return err
}
}
return nil
}
func (h *windowsAnsiEventHandler) EL(param int) error {
if err := h.Flush(); err != nil {
return err
}
logger.Infof("EL: [%v]", strconv.Itoa(param))
h.clearWrap()
// [K -- Erases from the cursor to the end of the line, including the cursor position.
// [1K -- Erases from the beginning of the line to the cursor, including the cursor position.
// [2K -- Erases the complete line.
info, err := GetConsoleScreenBufferInfo(h.fd)
if err != nil {
return err
}
var start COORD
var end COORD
switch param {
case 0:
start = info.CursorPosition
end = COORD{info.Size.X, info.CursorPosition.Y}
case 1:
start = COORD{0, info.CursorPosition.Y}
end = info.CursorPosition
case 2:
start = COORD{0, info.CursorPosition.Y}
end = COORD{info.Size.X, info.CursorPosition.Y}
}
err = h.clearRange(h.attributes, start, end)
if err != nil {
return err
}
return nil
}
func (h *windowsAnsiEventHandler) IL(param int) error {
if err := h.Flush(); err != nil {
return err
}
logger.Infof("IL: [%v]", strconv.Itoa(param))
h.clearWrap()
return h.insertLines(param)
}
func (h *windowsAnsiEventHandler) DL(param int) error {
if err := h.Flush(); err != nil {
return err
}
logger.Infof("DL: [%v]", strconv.Itoa(param))
h.clearWrap()
return h.deleteLines(param)
}
func (h *windowsAnsiEventHandler) ICH(param int) error {
if err := h.Flush(); err != nil {
return err
}
logger.Infof("ICH: [%v]", strconv.Itoa(param))
h.clearWrap()
return h.insertCharacters(param)
}
func (h *windowsAnsiEventHandler) DCH(param int) error {
if err := h.Flush(); err != nil {
return err
}
logger.Infof("DCH: [%v]", strconv.Itoa(param))
h.clearWrap()
return h.deleteCharacters(param)
}
func (h *windowsAnsiEventHandler) SGR(params []int) error {
if err := h.Flush(); err != nil {
return err
}
strings := []string{}
for _, v := range params {
strings = append(strings, strconv.Itoa(v))
}
logger.Infof("SGR: [%v]", strings)
if len(params) <= 0 {
h.attributes = h.infoReset.Attributes
h.inverted = false
} else {
for _, attr := range params {
if attr == ansiterm.ANSI_SGR_RESET {
h.attributes = h.infoReset.Attributes
h.inverted = false
continue
}
h.attributes, h.inverted = collectAnsiIntoWindowsAttributes(h.attributes, h.inverted, h.infoReset.Attributes, int16(attr))
}
}
attributes := h.attributes
if h.inverted {
attributes = invertAttributes(attributes)
}
err := SetConsoleTextAttribute(h.fd, attributes)
if err != nil {
return err
}
return nil
}
func (h *windowsAnsiEventHandler) SU(param int) error {
if err := h.Flush(); err != nil {
return err
}
logger.Infof("SU: [%v]", []string{strconv.Itoa(param)})
h.clearWrap()
return h.scrollUp(param)
}
func (h *windowsAnsiEventHandler) SD(param int) error {
if err := h.Flush(); err != nil {
return err
}
logger.Infof("SD: [%v]", []string{strconv.Itoa(param)})
h.clearWrap()
return h.scrollDown(param)
}
func (h *windowsAnsiEventHandler) DA(params []string) error {
logger.Infof("DA: [%v]", params)
// DA cannot be implemented because it must send data on the VT100 input stream,
// which is not available to go-ansiterm.
return nil
}
func (h *windowsAnsiEventHandler) DECSTBM(top int, bottom int) error {
if err := h.Flush(); err != nil {
return err
}
logger.Infof("DECSTBM: [%d, %d]", top, bottom)
// Windows is 0 indexed, Linux is 1 indexed
h.sr.top = int16(top - 1)
h.sr.bottom = int16(bottom - 1)
// This command also moves the cursor to the origin.
h.clearWrap()
return h.CUP(1, 1)
}
func (h *windowsAnsiEventHandler) RI() error {
if err := h.Flush(); err != nil {
return err
}
logger.Info("RI: []")
h.clearWrap()
info, err := GetConsoleScreenBufferInfo(h.fd)
if err != nil {
return err
}
sr := h.effectiveSr(info.Window)
if info.CursorPosition.Y == sr.top {
return h.scrollDown(1)
}
return h.moveCursorVertical(-1)
}
func (h *windowsAnsiEventHandler) IND() error {
logger.Info("IND: []")
return h.executeLF()
}
func (h *windowsAnsiEventHandler) Flush() error {
h.curInfo = nil
if h.buffer.Len() > 0 {
logger.Infof("Flush: [%s]", h.buffer.Bytes())
if _, err := h.buffer.WriteTo(h.file); err != nil {
return err
}
}
if h.wrapNext && !h.drewMarginByte {
logger.Infof("Flush: drawing margin byte '%c'", h.marginByte)
info, err := GetConsoleScreenBufferInfo(h.fd)
if err != nil {
return err
}
charInfo := []CHAR_INFO{{UnicodeChar: uint16(h.marginByte), Attributes: info.Attributes}}
size := COORD{1, 1}
position := COORD{0, 0}
region := SMALL_RECT{Left: info.CursorPosition.X, Top: info.CursorPosition.Y, Right: info.CursorPosition.X, Bottom: info.CursorPosition.Y}
if err := WriteConsoleOutput(h.fd, charInfo, size, position, &region); err != nil {
return err
}
h.drewMarginByte = true
}
return nil
}
// cacheConsoleInfo ensures that the current console screen information has been queried
// since the last call to Flush(). It must be called before accessing h.curInfo or h.curPos.
func (h *windowsAnsiEventHandler) getCurrentInfo() (COORD, *CONSOLE_SCREEN_BUFFER_INFO, error) {
if h.curInfo == nil {
info, err := GetConsoleScreenBufferInfo(h.fd)
if err != nil {
return COORD{}, nil, err
}
h.curInfo = info
h.curPos = info.CursorPosition
}
return h.curPos, h.curInfo, nil
}
func (h *windowsAnsiEventHandler) updatePos(pos COORD) {
if h.curInfo == nil {
panic("failed to call getCurrentInfo before calling updatePos")
}
h.curPos = pos
}
// clearWrap clears the state where the cursor is in the margin
// waiting for the next character before wrapping the line. This must
// be done before most operations that act on the cursor.
func (h *windowsAnsiEventHandler) clearWrap() {
h.wrapNext = false
h.drewMarginByte = false
}

View file

@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2015 Microsoft
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,266 @@
package winio
import (
"encoding/binary"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"runtime"
"syscall"
"unicode/utf16"
)
//sys backupRead(h syscall.Handle, b []byte, bytesRead *uint32, abort bool, processSecurity bool, context *uintptr) (err error) = BackupRead
//sys backupWrite(h syscall.Handle, b []byte, bytesWritten *uint32, abort bool, processSecurity bool, context *uintptr) (err error) = BackupWrite
const (
BackupData = uint32(iota + 1)
BackupEaData
BackupSecurity
BackupAlternateData
BackupLink
BackupPropertyData
BackupObjectId
BackupReparseData
BackupSparseBlock
BackupTxfsData
)
const (
StreamSparseAttributes = uint32(8)
)
const (
WRITE_DAC = 0x40000
WRITE_OWNER = 0x80000
ACCESS_SYSTEM_SECURITY = 0x1000000
)
// BackupHeader represents a backup stream of a file.
type BackupHeader struct {
Id uint32 // The backup stream ID
Attributes uint32 // Stream attributes
Size int64 // The size of the stream in bytes
Name string // The name of the stream (for BackupAlternateData only).
Offset int64 // The offset of the stream in the file (for BackupSparseBlock only).
}
type win32StreamId struct {
StreamId uint32
Attributes uint32
Size uint64
NameSize uint32
}
// BackupStreamReader reads from a stream produced by the BackupRead Win32 API and produces a series
// of BackupHeader values.
type BackupStreamReader struct {
r io.Reader
bytesLeft int64
}
// NewBackupStreamReader produces a BackupStreamReader from any io.Reader.
func NewBackupStreamReader(r io.Reader) *BackupStreamReader {
return &BackupStreamReader{r, 0}
}
// Next returns the next backup stream and prepares for calls to Write(). It skips the remainder of the current stream if
// it was not completely read.
func (r *BackupStreamReader) Next() (*BackupHeader, error) {
if r.bytesLeft > 0 {
if _, err := io.Copy(ioutil.Discard, r); err != nil {
return nil, err
}
}
var wsi win32StreamId
if err := binary.Read(r.r, binary.LittleEndian, &wsi); err != nil {
return nil, err
}
hdr := &BackupHeader{
Id: wsi.StreamId,
Attributes: wsi.Attributes,
Size: int64(wsi.Size),
}
if wsi.NameSize != 0 {
name := make([]uint16, int(wsi.NameSize/2))
if err := binary.Read(r.r, binary.LittleEndian, name); err != nil {
return nil, err
}
hdr.Name = syscall.UTF16ToString(name)
}
if wsi.StreamId == BackupSparseBlock {
if err := binary.Read(r.r, binary.LittleEndian, &hdr.Offset); err != nil {
return nil, err
}
hdr.Size -= 8
}
r.bytesLeft = hdr.Size
return hdr, nil
}
// Read reads from the current backup stream.
func (r *BackupStreamReader) Read(b []byte) (int, error) {
if r.bytesLeft == 0 {
return 0, io.EOF
}
if int64(len(b)) > r.bytesLeft {
b = b[:r.bytesLeft]
}
n, err := r.r.Read(b)
r.bytesLeft -= int64(n)
if err == io.EOF {
err = io.ErrUnexpectedEOF
} else if r.bytesLeft == 0 && err == nil {
err = io.EOF
}
return n, err
}
// BackupStreamWriter writes a stream compatible with the BackupWrite Win32 API.
type BackupStreamWriter struct {
w io.Writer
bytesLeft int64
}
// NewBackupStreamWriter produces a BackupStreamWriter on top of an io.Writer.
func NewBackupStreamWriter(w io.Writer) *BackupStreamWriter {
return &BackupStreamWriter{w, 0}
}
// WriteHeader writes the next backup stream header and prepares for calls to Write().
func (w *BackupStreamWriter) WriteHeader(hdr *BackupHeader) error {
if w.bytesLeft != 0 {
return fmt.Errorf("missing %d bytes", w.bytesLeft)
}
name := utf16.Encode([]rune(hdr.Name))
wsi := win32StreamId{
StreamId: hdr.Id,
Attributes: hdr.Attributes,
Size: uint64(hdr.Size),
NameSize: uint32(len(name) * 2),
}
if hdr.Id == BackupSparseBlock {
// Include space for the int64 block offset
wsi.Size += 8
}
if err := binary.Write(w.w, binary.LittleEndian, &wsi); err != nil {
return err
}
if len(name) != 0 {
if err := binary.Write(w.w, binary.LittleEndian, name); err != nil {
return err
}
}
if hdr.Id == BackupSparseBlock {
if err := binary.Write(w.w, binary.LittleEndian, hdr.Offset); err != nil {
return err
}
}
w.bytesLeft = hdr.Size
return nil
}
// Write writes to the current backup stream.
func (w *BackupStreamWriter) Write(b []byte) (int, error) {
if w.bytesLeft < int64(len(b)) {
return 0, fmt.Errorf("too many bytes by %d", int64(len(b))-w.bytesLeft)
}
n, err := w.w.Write(b)
w.bytesLeft -= int64(n)
return n, err
}
// BackupFileReader provides an io.ReadCloser interface on top of the BackupRead Win32 API.
type BackupFileReader struct {
f *os.File
includeSecurity bool
ctx uintptr
}
// NewBackupFileReader returns a new BackupFileReader from a file handle. If includeSecurity is true,
// Read will attempt to read the security descriptor of the file.
func NewBackupFileReader(f *os.File, includeSecurity bool) *BackupFileReader {
r := &BackupFileReader{f, includeSecurity, 0}
runtime.SetFinalizer(r, func(r *BackupFileReader) { r.Close() })
return r
}
// Read reads a backup stream from the file by calling the Win32 API BackupRead().
func (r *BackupFileReader) Read(b []byte) (int, error) {
var bytesRead uint32
err := backupRead(syscall.Handle(r.f.Fd()), b, &bytesRead, false, r.includeSecurity, &r.ctx)
if err != nil {
return 0, &os.PathError{"BackupRead", r.f.Name(), err}
}
if bytesRead == 0 {
return 0, io.EOF
}
return int(bytesRead), nil
}
// Close frees Win32 resources associated with the BackupFileReader. It does not close
// the underlying file.
func (r *BackupFileReader) Close() error {
if r.ctx != 0 {
backupRead(syscall.Handle(r.f.Fd()), nil, nil, true, false, &r.ctx)
r.ctx = 0
}
return nil
}
// BackupFileWriter provides an io.WriteCloser interface on top of the BackupWrite Win32 API.
type BackupFileWriter struct {
f *os.File
includeSecurity bool
ctx uintptr
}
// NewBackupFileWrtier returns a new BackupFileWriter from a file handle. If includeSecurity is true,
// Write() will attempt to restore the security descriptor from the stream.
func NewBackupFileWriter(f *os.File, includeSecurity bool) *BackupFileWriter {
w := &BackupFileWriter{f, includeSecurity, 0}
runtime.SetFinalizer(w, func(w *BackupFileWriter) { w.Close() })
return w
}
// Write restores a portion of the file using the provided backup stream.
func (w *BackupFileWriter) Write(b []byte) (int, error) {
var bytesWritten uint32
err := backupWrite(syscall.Handle(w.f.Fd()), b, &bytesWritten, false, w.includeSecurity, &w.ctx)
if err != nil {
return 0, &os.PathError{"BackupWrite", w.f.Name(), err}
}
if int(bytesWritten) != len(b) {
return int(bytesWritten), errors.New("not all bytes could be written")
}
return len(b), nil
}
// Close frees Win32 resources associated with the BackupFileWriter. It does not
// close the underlying file.
func (w *BackupFileWriter) Close() error {
if w.ctx != 0 {
backupWrite(syscall.Handle(w.f.Fd()), nil, nil, true, false, &w.ctx)
w.ctx = 0
}
return nil
}
// OpenForBackup opens a file or directory, potentially skipping access checks if the backup
// or restore privileges have been acquired.
//
// If the file opened was a directory, it cannot be used with Readdir().
func OpenForBackup(path string, access uint32, share uint32, createmode uint32) (*os.File, error) {
winPath, err := syscall.UTF16FromString(path)
if err != nil {
return nil, err
}
h, err := syscall.CreateFile(&winPath[0], access, share, nil, createmode, syscall.FILE_FLAG_BACKUP_SEMANTICS, 0)
if err != nil {
err = &os.PathError{Op: "open", Path: path, Err: err}
return nil, err
}
return os.NewFile(uintptr(h), path), nil
}

View file

@ -0,0 +1,219 @@
package winio
import (
"errors"
"io"
"runtime"
"sync"
"syscall"
"time"
)
//sys cancelIoEx(file syscall.Handle, o *syscall.Overlapped) (err error) = CancelIoEx
//sys createIoCompletionPort(file syscall.Handle, port syscall.Handle, key uintptr, threadCount uint32) (newport syscall.Handle, err error) = CreateIoCompletionPort
//sys getQueuedCompletionStatus(port syscall.Handle, bytes *uint32, key *uintptr, o **ioOperation, timeout uint32) (err error) = GetQueuedCompletionStatus
//sys setFileCompletionNotificationModes(h syscall.Handle, flags uint8) (err error) = SetFileCompletionNotificationModes
//sys timeBeginPeriod(period uint32) (n int32) = winmm.timeBeginPeriod
const (
cFILE_SKIP_COMPLETION_PORT_ON_SUCCESS = 1
cFILE_SKIP_SET_EVENT_ON_HANDLE = 2
)
var (
ErrFileClosed = errors.New("file has already been closed")
ErrTimeout = &timeoutError{}
)
type timeoutError struct{}
func (e *timeoutError) Error() string { return "i/o timeout" }
func (e *timeoutError) Timeout() bool { return true }
func (e *timeoutError) Temporary() bool { return true }
var ioInitOnce sync.Once
var ioCompletionPort syscall.Handle
// ioResult contains the result of an asynchronous IO operation
type ioResult struct {
bytes uint32
err error
}
// ioOperation represents an outstanding asynchronous Win32 IO
type ioOperation struct {
o syscall.Overlapped
ch chan ioResult
}
func initIo() {
h, err := createIoCompletionPort(syscall.InvalidHandle, 0, 0, 0xffffffff)
if err != nil {
panic(err)
}
ioCompletionPort = h
go ioCompletionProcessor(h)
}
// win32File implements Reader, Writer, and Closer on a Win32 handle without blocking in a syscall.
// It takes ownership of this handle and will close it if it is garbage collected.
type win32File struct {
handle syscall.Handle
wg sync.WaitGroup
closing bool
readDeadline time.Time
writeDeadline time.Time
}
// makeWin32File makes a new win32File from an existing file handle
func makeWin32File(h syscall.Handle) (*win32File, error) {
f := &win32File{handle: h}
ioInitOnce.Do(initIo)
_, err := createIoCompletionPort(h, ioCompletionPort, 0, 0xffffffff)
if err != nil {
return nil, err
}
err = setFileCompletionNotificationModes(h, cFILE_SKIP_COMPLETION_PORT_ON_SUCCESS|cFILE_SKIP_SET_EVENT_ON_HANDLE)
if err != nil {
return nil, err
}
runtime.SetFinalizer(f, (*win32File).closeHandle)
return f, nil
}
func MakeOpenFile(h syscall.Handle) (io.ReadWriteCloser, error) {
return makeWin32File(h)
}
// closeHandle closes the resources associated with a Win32 handle
func (f *win32File) closeHandle() {
if !f.closing {
// cancel all IO and wait for it to complete
f.closing = true
cancelIoEx(f.handle, nil)
f.wg.Wait()
// at this point, no new IO can start
syscall.Close(f.handle)
f.handle = 0
}
}
// Close closes a win32File.
func (f *win32File) Close() error {
f.closeHandle()
runtime.SetFinalizer(f, nil)
return nil
}
// prepareIo prepares for a new IO operation
func (f *win32File) prepareIo() (*ioOperation, error) {
f.wg.Add(1)
if f.closing {
return nil, ErrFileClosed
}
c := &ioOperation{}
c.ch = make(chan ioResult)
return c, nil
}
// ioCompletionProcessor processes completed async IOs forever
func ioCompletionProcessor(h syscall.Handle) {
// Set the timer resolution to 1. This fixes a performance regression in golang 1.6.
timeBeginPeriod(1)
for {
var bytes uint32
var key uintptr
var op *ioOperation
err := getQueuedCompletionStatus(h, &bytes, &key, &op, syscall.INFINITE)
if op == nil {
panic(err)
}
op.ch <- ioResult{bytes, err}
}
}
// asyncIo processes the return value from ReadFile or WriteFile, blocking until
// the operation has actually completed.
func (f *win32File) asyncIo(c *ioOperation, deadline time.Time, bytes uint32, err error) (int, error) {
if err != syscall.ERROR_IO_PENDING {
f.wg.Done()
return int(bytes), err
} else {
var r ioResult
wait := true
timedout := false
if f.closing {
cancelIoEx(f.handle, &c.o)
} else if !deadline.IsZero() {
now := time.Now()
if !deadline.After(now) {
timedout = true
} else {
timeout := time.After(deadline.Sub(now))
select {
case r = <-c.ch:
wait = false
case <-timeout:
timedout = true
}
}
}
if timedout {
cancelIoEx(f.handle, &c.o)
}
if wait {
r = <-c.ch
}
err = r.err
if err == syscall.ERROR_OPERATION_ABORTED {
if f.closing {
err = ErrFileClosed
} else if timedout {
err = ErrTimeout
}
}
f.wg.Done()
return int(r.bytes), err
}
}
// Read reads from a file handle.
func (f *win32File) Read(b []byte) (int, error) {
c, err := f.prepareIo()
if err != nil {
return 0, err
}
var bytes uint32
err = syscall.ReadFile(f.handle, b, &bytes, &c.o)
n, err := f.asyncIo(c, f.readDeadline, bytes, err)
// Handle EOF conditions.
if err == nil && n == 0 && len(b) != 0 {
return 0, io.EOF
} else if err == syscall.ERROR_BROKEN_PIPE {
return 0, io.EOF
} else {
return n, err
}
}
// Write writes to a file handle.
func (f *win32File) Write(b []byte) (int, error) {
c, err := f.prepareIo()
if err != nil {
return 0, err
}
var bytes uint32
err = syscall.WriteFile(f.handle, b, &bytes, &c.o)
return f.asyncIo(c, f.writeDeadline, bytes, err)
}
func (f *win32File) SetReadDeadline(t time.Time) error {
f.readDeadline = t
return nil
}
func (f *win32File) SetWriteDeadline(t time.Time) error {
f.writeDeadline = t
return nil
}

View file

@ -0,0 +1,54 @@
package winio
import (
"os"
"syscall"
"unsafe"
)
//sys getFileInformationByHandleEx(h syscall.Handle, class uint32, buffer *byte, size uint32) (err error) = GetFileInformationByHandleEx
//sys setFileInformationByHandle(h syscall.Handle, class uint32, buffer *byte, size uint32) (err error) = SetFileInformationByHandle
const (
fileBasicInfo = 0
fileIDInfo = 0x12
)
// FileBasicInfo contains file access time and file attributes information.
type FileBasicInfo struct {
CreationTime, LastAccessTime, LastWriteTime, ChangeTime syscall.Filetime
FileAttributes uintptr // includes padding
}
// GetFileBasicInfo retrieves times and attributes for a file.
func GetFileBasicInfo(f *os.File) (*FileBasicInfo, error) {
bi := &FileBasicInfo{}
if err := getFileInformationByHandleEx(syscall.Handle(f.Fd()), fileBasicInfo, (*byte)(unsafe.Pointer(bi)), uint32(unsafe.Sizeof(*bi))); err != nil {
return nil, &os.PathError{Op: "GetFileInformationByHandleEx", Path: f.Name(), Err: err}
}
return bi, nil
}
// SetFileBasicInfo sets times and attributes for a file.
func SetFileBasicInfo(f *os.File, bi *FileBasicInfo) error {
if err := setFileInformationByHandle(syscall.Handle(f.Fd()), fileBasicInfo, (*byte)(unsafe.Pointer(bi)), uint32(unsafe.Sizeof(*bi))); err != nil {
return &os.PathError{Op: "SetFileInformationByHandle", Path: f.Name(), Err: err}
}
return nil
}
// FileIDInfo contains the volume serial number and file ID for a file. This pair should be
// unique on a system.
type FileIDInfo struct {
VolumeSerialNumber uint64
FileID [16]byte
}
// GetFileID retrieves the unique (volume, file ID) pair for a file.
func GetFileID(f *os.File) (*FileIDInfo, error) {
fileID := &FileIDInfo{}
if err := getFileInformationByHandleEx(syscall.Handle(f.Fd()), fileIDInfo, (*byte)(unsafe.Pointer(fileID)), uint32(unsafe.Sizeof(*fileID))); err != nil {
return nil, &os.PathError{Op: "GetFileInformationByHandleEx", Path: f.Name(), Err: err}
}
return fileID, nil
}

View file

@ -0,0 +1,398 @@
package winio
import (
"errors"
"io"
"net"
"os"
"syscall"
"time"
"unsafe"
)
//sys connectNamedPipe(pipe syscall.Handle, o *syscall.Overlapped) (err error) = ConnectNamedPipe
//sys createNamedPipe(name string, flags uint32, pipeMode uint32, maxInstances uint32, outSize uint32, inSize uint32, defaultTimeout uint32, sa *securityAttributes) (handle syscall.Handle, err error) [failretval==syscall.InvalidHandle] = CreateNamedPipeW
//sys createFile(name string, access uint32, mode uint32, sa *securityAttributes, createmode uint32, attrs uint32, templatefile syscall.Handle) (handle syscall.Handle, err error) [failretval==syscall.InvalidHandle] = CreateFileW
//sys waitNamedPipe(name string, timeout uint32) (err error) = WaitNamedPipeW
//sys getNamedPipeInfo(pipe syscall.Handle, flags *uint32, outSize *uint32, inSize *uint32, maxInstances *uint32) (err error) = GetNamedPipeInfo
//sys getNamedPipeHandleState(pipe syscall.Handle, state *uint32, curInstances *uint32, maxCollectionCount *uint32, collectDataTimeout *uint32, userName *uint16, maxUserNameSize uint32) (err error) = GetNamedPipeHandleStateW
type securityAttributes struct {
Length uint32
SecurityDescriptor *byte
InheritHandle uint32
}
const (
cERROR_PIPE_BUSY = syscall.Errno(231)
cERROR_PIPE_CONNECTED = syscall.Errno(535)
cERROR_SEM_TIMEOUT = syscall.Errno(121)
cPIPE_ACCESS_DUPLEX = 0x3
cFILE_FLAG_FIRST_PIPE_INSTANCE = 0x80000
cSECURITY_SQOS_PRESENT = 0x100000
cSECURITY_ANONYMOUS = 0
cPIPE_REJECT_REMOTE_CLIENTS = 0x8
cPIPE_UNLIMITED_INSTANCES = 255
cNMPWAIT_USE_DEFAULT_WAIT = 0
cNMPWAIT_NOWAIT = 1
cPIPE_TYPE_MESSAGE = 4
cPIPE_READMODE_MESSAGE = 2
)
var (
// ErrPipeListenerClosed is returned for pipe operations on listeners that have been closed.
// This error should match net.errClosing since docker takes a dependency on its text.
ErrPipeListenerClosed = errors.New("use of closed network connection")
errPipeWriteClosed = errors.New("pipe has been closed for write")
)
type win32Pipe struct {
*win32File
path string
}
type win32MessageBytePipe struct {
win32Pipe
writeClosed bool
readEOF bool
}
type pipeAddress string
func (f *win32Pipe) LocalAddr() net.Addr {
return pipeAddress(f.path)
}
func (f *win32Pipe) RemoteAddr() net.Addr {
return pipeAddress(f.path)
}
func (f *win32Pipe) SetDeadline(t time.Time) error {
f.SetReadDeadline(t)
f.SetWriteDeadline(t)
return nil
}
// CloseWrite closes the write side of a message pipe in byte mode.
func (f *win32MessageBytePipe) CloseWrite() error {
if f.writeClosed {
return errPipeWriteClosed
}
_, err := f.win32File.Write(nil)
if err != nil {
return err
}
f.writeClosed = true
return nil
}
// Write writes bytes to a message pipe in byte mode. Zero-byte writes are ignored, since
// they are used to implement CloseWrite().
func (f *win32MessageBytePipe) Write(b []byte) (int, error) {
if f.writeClosed {
return 0, errPipeWriteClosed
}
if len(b) == 0 {
return 0, nil
}
return f.win32File.Write(b)
}
// Read reads bytes from a message pipe in byte mode. A read of a zero-byte message on a message
// mode pipe will return io.EOF, as will all subsequent reads.
func (f *win32MessageBytePipe) Read(b []byte) (int, error) {
if f.readEOF {
return 0, io.EOF
}
n, err := f.win32File.Read(b)
if err == io.EOF {
// If this was the result of a zero-byte read, then
// it is possible that the read was due to a zero-size
// message. Since we are simulating CloseWrite with a
// zero-byte message, ensure that all future Read() calls
// also return EOF.
f.readEOF = true
}
return n, err
}
func (s pipeAddress) Network() string {
return "pipe"
}
func (s pipeAddress) String() string {
return string(s)
}
// DialPipe connects to a named pipe by path, timing out if the connection
// takes longer than the specified duration. If timeout is nil, then the timeout
// is the default timeout established by the pipe server.
func DialPipe(path string, timeout *time.Duration) (net.Conn, error) {
var absTimeout time.Time
if timeout != nil {
absTimeout = time.Now().Add(*timeout)
}
var err error
var h syscall.Handle
for {
h, err = createFile(path, syscall.GENERIC_READ|syscall.GENERIC_WRITE, 0, nil, syscall.OPEN_EXISTING, syscall.FILE_FLAG_OVERLAPPED|cSECURITY_SQOS_PRESENT|cSECURITY_ANONYMOUS, 0)
if err != cERROR_PIPE_BUSY {
break
}
now := time.Now()
var ms uint32
if absTimeout.IsZero() {
ms = cNMPWAIT_USE_DEFAULT_WAIT
} else if now.After(absTimeout) {
ms = cNMPWAIT_NOWAIT
} else {
ms = uint32(absTimeout.Sub(now).Nanoseconds() / 1000 / 1000)
}
err = waitNamedPipe(path, ms)
if err != nil {
if err == cERROR_SEM_TIMEOUT {
return nil, ErrTimeout
}
break
}
}
if err != nil {
return nil, &os.PathError{Op: "open", Path: path, Err: err}
}
var flags uint32
err = getNamedPipeInfo(h, &flags, nil, nil, nil)
if err != nil {
return nil, err
}
var state uint32
err = getNamedPipeHandleState(h, &state, nil, nil, nil, nil, 0)
if err != nil {
return nil, err
}
if state&cPIPE_READMODE_MESSAGE != 0 {
return nil, &os.PathError{Op: "open", Path: path, Err: errors.New("message readmode pipes not supported")}
}
f, err := makeWin32File(h)
if err != nil {
syscall.Close(h)
return nil, err
}
// If the pipe is in message mode, return a message byte pipe, which
// supports CloseWrite().
if flags&cPIPE_TYPE_MESSAGE != 0 {
return &win32MessageBytePipe{
win32Pipe: win32Pipe{win32File: f, path: path},
}, nil
}
return &win32Pipe{win32File: f, path: path}, nil
}
type acceptResponse struct {
f *win32File
err error
}
type win32PipeListener struct {
firstHandle syscall.Handle
path string
securityDescriptor []byte
config PipeConfig
acceptCh chan (chan acceptResponse)
closeCh chan int
doneCh chan int
}
func makeServerPipeHandle(path string, securityDescriptor []byte, c *PipeConfig, first bool) (syscall.Handle, error) {
var flags uint32 = cPIPE_ACCESS_DUPLEX | syscall.FILE_FLAG_OVERLAPPED
if first {
flags |= cFILE_FLAG_FIRST_PIPE_INSTANCE
}
var mode uint32 = cPIPE_REJECT_REMOTE_CLIENTS
if c.MessageMode {
mode |= cPIPE_TYPE_MESSAGE
}
var sa securityAttributes
sa.Length = uint32(unsafe.Sizeof(sa))
if securityDescriptor != nil {
sa.SecurityDescriptor = &securityDescriptor[0]
}
h, err := createNamedPipe(path, flags, mode, cPIPE_UNLIMITED_INSTANCES, uint32(c.OutputBufferSize), uint32(c.InputBufferSize), 0, &sa)
if err != nil {
return 0, &os.PathError{Op: "open", Path: path, Err: err}
}
return h, nil
}
func (l *win32PipeListener) makeServerPipe() (*win32File, error) {
h, err := makeServerPipeHandle(l.path, l.securityDescriptor, &l.config, false)
if err != nil {
return nil, err
}
f, err := makeWin32File(h)
if err != nil {
syscall.Close(h)
return nil, err
}
return f, nil
}
func (l *win32PipeListener) listenerRoutine() {
closed := false
for !closed {
select {
case <-l.closeCh:
closed = true
case responseCh := <-l.acceptCh:
p, err := l.makeServerPipe()
if err == nil {
// Wait for the client to connect.
ch := make(chan error)
go func() {
ch <- connectPipe(p)
}()
select {
case err = <-ch:
if err != nil {
p.Close()
p = nil
}
case <-l.closeCh:
// Abort the connect request by closing the handle.
p.Close()
p = nil
err = <-ch
if err == nil || err == ErrFileClosed {
err = ErrPipeListenerClosed
}
closed = true
}
}
responseCh <- acceptResponse{p, err}
}
}
syscall.Close(l.firstHandle)
l.firstHandle = 0
// Notify Close() and Accept() callers that the handle has been closed.
close(l.doneCh)
}
// PipeConfig contain configuration for the pipe listener.
type PipeConfig struct {
// SecurityDescriptor contains a Windows security descriptor in SDDL format.
SecurityDescriptor string
// MessageMode determines whether the pipe is in byte or message mode. In either
// case the pipe is read in byte mode by default. The only practical difference in
// this implementation is that CloseWrite() is only supported for message mode pipes;
// CloseWrite() is implemented as a zero-byte write, but zero-byte writes are only
// transferred to the reader (and returned as io.EOF in this implementation)
// when the pipe is in message mode.
MessageMode bool
// InputBufferSize specifies the size the input buffer, in bytes.
InputBufferSize int32
// OutputBufferSize specifies the size the input buffer, in bytes.
OutputBufferSize int32
}
// ListenPipe creates a listener on a Windows named pipe path, e.g. \\.\pipe\mypipe.
// The pipe must not already exist.
func ListenPipe(path string, c *PipeConfig) (net.Listener, error) {
var (
sd []byte
err error
)
if c == nil {
c = &PipeConfig{}
}
if c.SecurityDescriptor != "" {
sd, err = SddlToSecurityDescriptor(c.SecurityDescriptor)
if err != nil {
return nil, err
}
}
h, err := makeServerPipeHandle(path, sd, c, true)
if err != nil {
return nil, err
}
// Immediately open and then close a client handle so that the named pipe is
// created but not currently accepting connections.
h2, err := createFile(path, 0, 0, nil, syscall.OPEN_EXISTING, cSECURITY_SQOS_PRESENT|cSECURITY_ANONYMOUS, 0)
if err != nil {
syscall.Close(h)
return nil, err
}
syscall.Close(h2)
l := &win32PipeListener{
firstHandle: h,
path: path,
securityDescriptor: sd,
config: *c,
acceptCh: make(chan (chan acceptResponse)),
closeCh: make(chan int),
doneCh: make(chan int),
}
go l.listenerRoutine()
return l, nil
}
func connectPipe(p *win32File) error {
c, err := p.prepareIo()
if err != nil {
return err
}
err = connectNamedPipe(p.handle, &c.o)
_, err = p.asyncIo(c, time.Time{}, 0, err)
if err != nil && err != cERROR_PIPE_CONNECTED {
return err
}
return nil
}
func (l *win32PipeListener) Accept() (net.Conn, error) {
ch := make(chan acceptResponse)
select {
case l.acceptCh <- ch:
response := <-ch
err := response.err
if err != nil {
return nil, err
}
if l.config.MessageMode {
return &win32MessageBytePipe{
win32Pipe: win32Pipe{win32File: response.f, path: l.path},
}, nil
}
return &win32Pipe{win32File: response.f, path: l.path}, nil
case <-l.doneCh:
return nil, ErrPipeListenerClosed
}
}
func (l *win32PipeListener) Close() error {
select {
case l.closeCh <- 1:
<-l.doneCh
case <-l.doneCh:
}
return nil
}
func (l *win32PipeListener) Addr() net.Addr {
return pipeAddress(l.path)
}

View file

@ -0,0 +1,150 @@
package winio
import (
"bytes"
"encoding/binary"
"fmt"
"runtime"
"syscall"
"unicode/utf16"
)
//sys adjustTokenPrivileges(token syscall.Handle, releaseAll bool, input *byte, outputSize uint32, output *byte, requiredSize *uint32) (success bool, err error) [true] = advapi32.AdjustTokenPrivileges
//sys impersonateSelf(level uint32) (err error) = advapi32.ImpersonateSelf
//sys revertToSelf() (err error) = advapi32.RevertToSelf
//sys openThreadToken(thread syscall.Handle, accessMask uint32, openAsSelf bool, token *syscall.Handle) (err error) = advapi32.OpenThreadToken
//sys getCurrentThread() (h syscall.Handle) = GetCurrentThread
//sys lookupPrivilegeValue(systemName string, name string, luid *uint64) (err error) = advapi32.LookupPrivilegeValueW
//sys lookupPrivilegeName(systemName string, luid *uint64, buffer *uint16, size *uint32) (err error) = advapi32.LookupPrivilegeNameW
//sys lookupPrivilegeDisplayName(systemName string, name *uint16, buffer *uint16, size *uint32, languageId *uint32) (err error) = advapi32.LookupPrivilegeDisplayNameW
const (
SE_PRIVILEGE_ENABLED = 2
ERROR_NOT_ALL_ASSIGNED syscall.Errno = 1300
SeBackupPrivilege = "SeBackupPrivilege"
SeRestorePrivilege = "SeRestorePrivilege"
)
const (
securityAnonymous = iota
securityIdentification
securityImpersonation
securityDelegation
)
type PrivilegeError struct {
privileges []uint64
}
func (e *PrivilegeError) Error() string {
s := ""
if len(e.privileges) > 1 {
s = "Could not enable privileges "
} else {
s = "Could not enable privilege "
}
for i, p := range e.privileges {
if i != 0 {
s += ", "
}
s += `"`
s += getPrivilegeName(p)
s += `"`
}
return s
}
func RunWithPrivilege(name string, fn func() error) error {
return RunWithPrivileges([]string{name}, fn)
}
func RunWithPrivileges(names []string, fn func() error) error {
var privileges []uint64
for _, name := range names {
p := uint64(0)
err := lookupPrivilegeValue("", name, &p)
if err != nil {
return err
}
privileges = append(privileges, p)
}
runtime.LockOSThread()
defer runtime.UnlockOSThread()
token, err := newThreadToken()
if err != nil {
return err
}
defer releaseThreadToken(token)
err = adjustPrivileges(token, privileges)
if err != nil {
return err
}
return fn()
}
func adjustPrivileges(token syscall.Handle, privileges []uint64) error {
var b bytes.Buffer
binary.Write(&b, binary.LittleEndian, uint32(len(privileges)))
for _, p := range privileges {
binary.Write(&b, binary.LittleEndian, p)
binary.Write(&b, binary.LittleEndian, uint32(SE_PRIVILEGE_ENABLED))
}
prevState := make([]byte, b.Len())
reqSize := uint32(0)
success, err := adjustTokenPrivileges(token, false, &b.Bytes()[0], uint32(len(prevState)), &prevState[0], &reqSize)
if !success {
return err
}
if err == ERROR_NOT_ALL_ASSIGNED {
return &PrivilegeError{privileges}
}
return nil
}
func getPrivilegeName(luid uint64) string {
var nameBuffer [256]uint16
bufSize := uint32(len(nameBuffer))
err := lookupPrivilegeName("", &luid, &nameBuffer[0], &bufSize)
if err != nil {
return fmt.Sprintf("<unknown privilege %d>", luid)
}
var displayNameBuffer [256]uint16
displayBufSize := uint32(len(displayNameBuffer))
var langId uint32
err = lookupPrivilegeDisplayName("", &nameBuffer[0], &displayNameBuffer[0], &displayBufSize, &langId)
if err != nil {
return fmt.Sprintf("<unknown privilege %s>", utf16.Decode(nameBuffer[:bufSize]))
}
return string(utf16.Decode(displayNameBuffer[:displayBufSize]))
}
func newThreadToken() (syscall.Handle, error) {
err := impersonateSelf(securityImpersonation)
if err != nil {
panic(err)
return 0, err
}
var token syscall.Handle
err = openThreadToken(getCurrentThread(), syscall.TOKEN_ADJUST_PRIVILEGES|syscall.TOKEN_QUERY, false, &token)
if err != nil {
rerr := revertToSelf()
if rerr != nil {
panic(rerr)
}
return 0, err
}
return token, nil
}
func releaseThreadToken(h syscall.Handle) {
err := revertToSelf()
if err != nil {
panic(err)
}
syscall.Close(h)
}

View file

@ -0,0 +1,128 @@
package winio
import (
"bytes"
"encoding/binary"
"fmt"
"strings"
"unicode/utf16"
"unsafe"
)
const (
reparseTagMountPoint = 0xA0000003
reparseTagSymlink = 0xA000000C
)
type reparseDataBuffer struct {
ReparseTag uint32
ReparseDataLength uint16
Reserved uint16
SubstituteNameOffset uint16
SubstituteNameLength uint16
PrintNameOffset uint16
PrintNameLength uint16
}
// ReparsePoint describes a Win32 symlink or mount point.
type ReparsePoint struct {
Target string
IsMountPoint bool
}
// UnsupportedReparsePointError is returned when trying to decode a non-symlink or
// mount point reparse point.
type UnsupportedReparsePointError struct {
Tag uint32
}
func (e *UnsupportedReparsePointError) Error() string {
return fmt.Sprintf("unsupported reparse point %x", e.Tag)
}
// DecodeReparsePoint decodes a Win32 REPARSE_DATA_BUFFER structure containing either a symlink
// or a mount point.
func DecodeReparsePoint(b []byte) (*ReparsePoint, error) {
tag := binary.LittleEndian.Uint32(b[0:4])
return DecodeReparsePointData(tag, b[8:])
}
func DecodeReparsePointData(tag uint32, b []byte) (*ReparsePoint, error) {
isMountPoint := false
switch tag {
case reparseTagMountPoint:
isMountPoint = true
case reparseTagSymlink:
default:
return nil, &UnsupportedReparsePointError{tag}
}
nameOffset := 8 + binary.LittleEndian.Uint16(b[4:6])
if !isMountPoint {
nameOffset += 4
}
nameLength := binary.LittleEndian.Uint16(b[6:8])
name := make([]uint16, nameLength/2)
err := binary.Read(bytes.NewReader(b[nameOffset:nameOffset+nameLength]), binary.LittleEndian, &name)
if err != nil {
return nil, err
}
return &ReparsePoint{string(utf16.Decode(name)), isMountPoint}, nil
}
func isDriveLetter(c byte) bool {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
}
// EncodeReparsePoint encodes a Win32 REPARSE_DATA_BUFFER structure describing a symlink or
// mount point.
func EncodeReparsePoint(rp *ReparsePoint) []byte {
// Generate an NT path and determine if this is a relative path.
var ntTarget string
relative := false
if strings.HasPrefix(rp.Target, `\\?\`) {
ntTarget = rp.Target
} else if strings.HasPrefix(rp.Target, `\\`) {
ntTarget = `\??\UNC\` + rp.Target[2:]
} else if len(rp.Target) >= 2 && isDriveLetter(rp.Target[0]) && rp.Target[1] == ':' {
ntTarget = `\??\` + rp.Target
} else {
ntTarget = rp.Target
relative = true
}
// The paths must be NUL-terminated even though they are counted strings.
target16 := utf16.Encode([]rune(rp.Target + "\x00"))
ntTarget16 := utf16.Encode([]rune(ntTarget + "\x00"))
size := int(unsafe.Sizeof(reparseDataBuffer{})) - 8
size += len(ntTarget16)*2 + len(target16)*2
tag := uint32(reparseTagMountPoint)
if !rp.IsMountPoint {
tag = reparseTagSymlink
size += 4 // Add room for symlink flags
}
data := reparseDataBuffer{
ReparseTag: tag,
ReparseDataLength: uint16(size),
SubstituteNameOffset: 0,
SubstituteNameLength: uint16((len(ntTarget16) - 1) * 2),
PrintNameOffset: uint16(len(ntTarget16) * 2),
PrintNameLength: uint16((len(target16) - 1) * 2),
}
var b bytes.Buffer
binary.Write(&b, binary.LittleEndian, &data)
if !rp.IsMountPoint {
flags := uint32(0)
if relative {
flags |= 1
}
binary.Write(&b, binary.LittleEndian, flags)
}
binary.Write(&b, binary.LittleEndian, ntTarget16)
binary.Write(&b, binary.LittleEndian, target16)
return b.Bytes()
}

View file

@ -0,0 +1,96 @@
package winio
import (
"syscall"
"unsafe"
)
//sys lookupAccountName(systemName *uint16, accountName string, sid *byte, sidSize *uint32, refDomain *uint16, refDomainSize *uint32, sidNameUse *uint32) (err error) = advapi32.LookupAccountNameW
//sys convertSidToStringSid(sid *byte, str **uint16) (err error) = advapi32.ConvertSidToStringSidW
//sys convertStringSecurityDescriptorToSecurityDescriptor(str string, revision uint32, sd *uintptr, size *uint32) (err error) = advapi32.ConvertStringSecurityDescriptorToSecurityDescriptorW
//sys convertSecurityDescriptorToStringSecurityDescriptor(sd *byte, revision uint32, secInfo uint32, sddl **uint16, sddlSize *uint32) (err error) = advapi32.ConvertSecurityDescriptorToStringSecurityDescriptorW
//sys localFree(mem uintptr) = LocalFree
//sys getSecurityDescriptorLength(sd uintptr) (len uint32) = advapi32.GetSecurityDescriptorLength
const (
cERROR_NONE_MAPPED = syscall.Errno(1332)
)
type AccountLookupError struct {
Name string
Err error
}
func (e *AccountLookupError) Error() string {
if e.Name == "" {
return "lookup account: empty account name specified"
}
var s string
switch e.Err {
case cERROR_NONE_MAPPED:
s = "not found"
default:
s = e.Err.Error()
}
return "lookup account " + e.Name + ": " + s
}
type SddlConversionError struct {
Sddl string
Err error
}
func (e *SddlConversionError) Error() string {
return "convert " + e.Sddl + ": " + e.Err.Error()
}
// LookupSidByName looks up the SID of an account by name
func LookupSidByName(name string) (sid string, err error) {
if name == "" {
return "", &AccountLookupError{name, cERROR_NONE_MAPPED}
}
var sidSize, sidNameUse, refDomainSize uint32
err = lookupAccountName(nil, name, nil, &sidSize, nil, &refDomainSize, &sidNameUse)
if err != nil && err != syscall.ERROR_INSUFFICIENT_BUFFER {
return "", &AccountLookupError{name, err}
}
sidBuffer := make([]byte, sidSize)
refDomainBuffer := make([]uint16, refDomainSize)
err = lookupAccountName(nil, name, &sidBuffer[0], &sidSize, &refDomainBuffer[0], &refDomainSize, &sidNameUse)
if err != nil {
return "", &AccountLookupError{name, err}
}
var strBuffer *uint16
err = convertSidToStringSid(&sidBuffer[0], &strBuffer)
if err != nil {
return "", &AccountLookupError{name, err}
}
sid = syscall.UTF16ToString((*[0xffff]uint16)(unsafe.Pointer(strBuffer))[:])
localFree(uintptr(unsafe.Pointer(strBuffer)))
return sid, nil
}
func SddlToSecurityDescriptor(sddl string) ([]byte, error) {
var sdBuffer uintptr
err := convertStringSecurityDescriptorToSecurityDescriptor(sddl, 1, &sdBuffer, nil)
if err != nil {
return nil, &SddlConversionError{sddl, err}
}
defer localFree(sdBuffer)
sd := make([]byte, getSecurityDescriptorLength(sdBuffer))
copy(sd, (*[0xffff]byte)(unsafe.Pointer(sdBuffer))[:len(sd)])
return sd, nil
}
func SecurityDescriptorToSddl(sd []byte) (string, error) {
var sddl *uint16
// The returned string length seems to including an aribtrary number of terminating NULs.
// Don't use it.
err := convertSecurityDescriptorToStringSecurityDescriptor(&sd[0], 1, 0xff, &sddl, nil)
if err != nil {
return "", err
}
defer localFree(uintptr(unsafe.Pointer(sddl)))
return syscall.UTF16ToString((*[0xffff]uint16)(unsafe.Pointer(sddl))[:]), nil
}

View file

@ -0,0 +1,3 @@
package winio
//go:generate go run $GOROOT/src/syscall/mksyscall_windows.go -output zsyscall.go file.go pipe.go sd.go fileinfo.go privilege.go backup.go

View file

@ -0,0 +1,492 @@
// MACHINE GENERATED BY 'go generate' COMMAND; DO NOT EDIT
package winio
import "unsafe"
import "syscall"
var _ unsafe.Pointer
var (
modkernel32 = syscall.NewLazyDLL("kernel32.dll")
modwinmm = syscall.NewLazyDLL("winmm.dll")
modadvapi32 = syscall.NewLazyDLL("advapi32.dll")
procCancelIoEx = modkernel32.NewProc("CancelIoEx")
procCreateIoCompletionPort = modkernel32.NewProc("CreateIoCompletionPort")
procGetQueuedCompletionStatus = modkernel32.NewProc("GetQueuedCompletionStatus")
procSetFileCompletionNotificationModes = modkernel32.NewProc("SetFileCompletionNotificationModes")
proctimeBeginPeriod = modwinmm.NewProc("timeBeginPeriod")
procConnectNamedPipe = modkernel32.NewProc("ConnectNamedPipe")
procCreateNamedPipeW = modkernel32.NewProc("CreateNamedPipeW")
procCreateFileW = modkernel32.NewProc("CreateFileW")
procWaitNamedPipeW = modkernel32.NewProc("WaitNamedPipeW")
procGetNamedPipeInfo = modkernel32.NewProc("GetNamedPipeInfo")
procGetNamedPipeHandleStateW = modkernel32.NewProc("GetNamedPipeHandleStateW")
procLookupAccountNameW = modadvapi32.NewProc("LookupAccountNameW")
procConvertSidToStringSidW = modadvapi32.NewProc("ConvertSidToStringSidW")
procConvertStringSecurityDescriptorToSecurityDescriptorW = modadvapi32.NewProc("ConvertStringSecurityDescriptorToSecurityDescriptorW")
procConvertSecurityDescriptorToStringSecurityDescriptorW = modadvapi32.NewProc("ConvertSecurityDescriptorToStringSecurityDescriptorW")
procLocalFree = modkernel32.NewProc("LocalFree")
procGetSecurityDescriptorLength = modadvapi32.NewProc("GetSecurityDescriptorLength")
procGetFileInformationByHandleEx = modkernel32.NewProc("GetFileInformationByHandleEx")
procSetFileInformationByHandle = modkernel32.NewProc("SetFileInformationByHandle")
procAdjustTokenPrivileges = modadvapi32.NewProc("AdjustTokenPrivileges")
procImpersonateSelf = modadvapi32.NewProc("ImpersonateSelf")
procRevertToSelf = modadvapi32.NewProc("RevertToSelf")
procOpenThreadToken = modadvapi32.NewProc("OpenThreadToken")
procGetCurrentThread = modkernel32.NewProc("GetCurrentThread")
procLookupPrivilegeValueW = modadvapi32.NewProc("LookupPrivilegeValueW")
procLookupPrivilegeNameW = modadvapi32.NewProc("LookupPrivilegeNameW")
procLookupPrivilegeDisplayNameW = modadvapi32.NewProc("LookupPrivilegeDisplayNameW")
procBackupRead = modkernel32.NewProc("BackupRead")
procBackupWrite = modkernel32.NewProc("BackupWrite")
)
func cancelIoEx(file syscall.Handle, o *syscall.Overlapped) (err error) {
r1, _, e1 := syscall.Syscall(procCancelIoEx.Addr(), 2, uintptr(file), uintptr(unsafe.Pointer(o)), 0)
if r1 == 0 {
if e1 != 0 {
err = error(e1)
} else {
err = syscall.EINVAL
}
}
return
}
func createIoCompletionPort(file syscall.Handle, port syscall.Handle, key uintptr, threadCount uint32) (newport syscall.Handle, err error) {
r0, _, e1 := syscall.Syscall6(procCreateIoCompletionPort.Addr(), 4, uintptr(file), uintptr(port), uintptr(key), uintptr(threadCount), 0, 0)
newport = syscall.Handle(r0)
if newport == 0 {
if e1 != 0 {
err = error(e1)
} else {
err = syscall.EINVAL
}
}
return
}
func getQueuedCompletionStatus(port syscall.Handle, bytes *uint32, key *uintptr, o **ioOperation, timeout uint32) (err error) {
r1, _, e1 := syscall.Syscall6(procGetQueuedCompletionStatus.Addr(), 5, uintptr(port), uintptr(unsafe.Pointer(bytes)), uintptr(unsafe.Pointer(key)), uintptr(unsafe.Pointer(o)), uintptr(timeout), 0)
if r1 == 0 {
if e1 != 0 {
err = error(e1)
} else {
err = syscall.EINVAL
}
}
return
}
func setFileCompletionNotificationModes(h syscall.Handle, flags uint8) (err error) {
r1, _, e1 := syscall.Syscall(procSetFileCompletionNotificationModes.Addr(), 2, uintptr(h), uintptr(flags), 0)
if r1 == 0 {
if e1 != 0 {
err = error(e1)
} else {
err = syscall.EINVAL
}
}
return
}
func timeBeginPeriod(period uint32) (n int32) {
r0, _, _ := syscall.Syscall(proctimeBeginPeriod.Addr(), 1, uintptr(period), 0, 0)
n = int32(r0)
return
}
func connectNamedPipe(pipe syscall.Handle, o *syscall.Overlapped) (err error) {
r1, _, e1 := syscall.Syscall(procConnectNamedPipe.Addr(), 2, uintptr(pipe), uintptr(unsafe.Pointer(o)), 0)
if r1 == 0 {
if e1 != 0 {
err = error(e1)
} else {
err = syscall.EINVAL
}
}
return
}
func createNamedPipe(name string, flags uint32, pipeMode uint32, maxInstances uint32, outSize uint32, inSize uint32, defaultTimeout uint32, sa *securityAttributes) (handle syscall.Handle, err error) {
var _p0 *uint16
_p0, err = syscall.UTF16PtrFromString(name)
if err != nil {
return
}
return _createNamedPipe(_p0, flags, pipeMode, maxInstances, outSize, inSize, defaultTimeout, sa)
}
func _createNamedPipe(name *uint16, flags uint32, pipeMode uint32, maxInstances uint32, outSize uint32, inSize uint32, defaultTimeout uint32, sa *securityAttributes) (handle syscall.Handle, err error) {
r0, _, e1 := syscall.Syscall9(procCreateNamedPipeW.Addr(), 8, uintptr(unsafe.Pointer(name)), uintptr(flags), uintptr(pipeMode), uintptr(maxInstances), uintptr(outSize), uintptr(inSize), uintptr(defaultTimeout), uintptr(unsafe.Pointer(sa)), 0)
handle = syscall.Handle(r0)
if handle == syscall.InvalidHandle {
if e1 != 0 {
err = error(e1)
} else {
err = syscall.EINVAL
}
}
return
}
func createFile(name string, access uint32, mode uint32, sa *securityAttributes, createmode uint32, attrs uint32, templatefile syscall.Handle) (handle syscall.Handle, err error) {
var _p0 *uint16
_p0, err = syscall.UTF16PtrFromString(name)
if err != nil {
return
}
return _createFile(_p0, access, mode, sa, createmode, attrs, templatefile)
}
func _createFile(name *uint16, access uint32, mode uint32, sa *securityAttributes, createmode uint32, attrs uint32, templatefile syscall.Handle) (handle syscall.Handle, err error) {
r0, _, e1 := syscall.Syscall9(procCreateFileW.Addr(), 7, uintptr(unsafe.Pointer(name)), uintptr(access), uintptr(mode), uintptr(unsafe.Pointer(sa)), uintptr(createmode), uintptr(attrs), uintptr(templatefile), 0, 0)
handle = syscall.Handle(r0)
if handle == syscall.InvalidHandle {
if e1 != 0 {
err = error(e1)
} else {
err = syscall.EINVAL
}
}
return
}
func waitNamedPipe(name string, timeout uint32) (err error) {
var _p0 *uint16
_p0, err = syscall.UTF16PtrFromString(name)
if err != nil {
return
}
return _waitNamedPipe(_p0, timeout)
}
func _waitNamedPipe(name *uint16, timeout uint32) (err error) {
r1, _, e1 := syscall.Syscall(procWaitNamedPipeW.Addr(), 2, uintptr(unsafe.Pointer(name)), uintptr(timeout), 0)
if r1 == 0 {
if e1 != 0 {
err = error(e1)
} else {
err = syscall.EINVAL
}
}
return
}
func getNamedPipeInfo(pipe syscall.Handle, flags *uint32, outSize *uint32, inSize *uint32, maxInstances *uint32) (err error) {
r1, _, e1 := syscall.Syscall6(procGetNamedPipeInfo.Addr(), 5, uintptr(pipe), uintptr(unsafe.Pointer(flags)), uintptr(unsafe.Pointer(outSize)), uintptr(unsafe.Pointer(inSize)), uintptr(unsafe.Pointer(maxInstances)), 0)
if r1 == 0 {
if e1 != 0 {
err = error(e1)
} else {
err = syscall.EINVAL
}
}
return
}
func getNamedPipeHandleState(pipe syscall.Handle, state *uint32, curInstances *uint32, maxCollectionCount *uint32, collectDataTimeout *uint32, userName *uint16, maxUserNameSize uint32) (err error) {
r1, _, e1 := syscall.Syscall9(procGetNamedPipeHandleStateW.Addr(), 7, uintptr(pipe), uintptr(unsafe.Pointer(state)), uintptr(unsafe.Pointer(curInstances)), uintptr(unsafe.Pointer(maxCollectionCount)), uintptr(unsafe.Pointer(collectDataTimeout)), uintptr(unsafe.Pointer(userName)), uintptr(maxUserNameSize), 0, 0)
if r1 == 0 {
if e1 != 0 {
err = error(e1)
} else {
err = syscall.EINVAL
}
}
return
}
func lookupAccountName(systemName *uint16, accountName string, sid *byte, sidSize *uint32, refDomain *uint16, refDomainSize *uint32, sidNameUse *uint32) (err error) {
var _p0 *uint16
_p0, err = syscall.UTF16PtrFromString(accountName)
if err != nil {
return
}
return _lookupAccountName(systemName, _p0, sid, sidSize, refDomain, refDomainSize, sidNameUse)
}
func _lookupAccountName(systemName *uint16, accountName *uint16, sid *byte, sidSize *uint32, refDomain *uint16, refDomainSize *uint32, sidNameUse *uint32) (err error) {
r1, _, e1 := syscall.Syscall9(procLookupAccountNameW.Addr(), 7, uintptr(unsafe.Pointer(systemName)), uintptr(unsafe.Pointer(accountName)), uintptr(unsafe.Pointer(sid)), uintptr(unsafe.Pointer(sidSize)), uintptr(unsafe.Pointer(refDomain)), uintptr(unsafe.Pointer(refDomainSize)), uintptr(unsafe.Pointer(sidNameUse)), 0, 0)
if r1 == 0 {
if e1 != 0 {
err = error(e1)
} else {
err = syscall.EINVAL
}
}
return
}
func convertSidToStringSid(sid *byte, str **uint16) (err error) {
r1, _, e1 := syscall.Syscall(procConvertSidToStringSidW.Addr(), 2, uintptr(unsafe.Pointer(sid)), uintptr(unsafe.Pointer(str)), 0)
if r1 == 0 {
if e1 != 0 {
err = error(e1)
} else {
err = syscall.EINVAL
}
}
return
}
func convertStringSecurityDescriptorToSecurityDescriptor(str string, revision uint32, sd *uintptr, size *uint32) (err error) {
var _p0 *uint16
_p0, err = syscall.UTF16PtrFromString(str)
if err != nil {
return
}
return _convertStringSecurityDescriptorToSecurityDescriptor(_p0, revision, sd, size)
}
func _convertStringSecurityDescriptorToSecurityDescriptor(str *uint16, revision uint32, sd *uintptr, size *uint32) (err error) {
r1, _, e1 := syscall.Syscall6(procConvertStringSecurityDescriptorToSecurityDescriptorW.Addr(), 4, uintptr(unsafe.Pointer(str)), uintptr(revision), uintptr(unsafe.Pointer(sd)), uintptr(unsafe.Pointer(size)), 0, 0)
if r1 == 0 {
if e1 != 0 {
err = error(e1)
} else {
err = syscall.EINVAL
}
}
return
}
func convertSecurityDescriptorToStringSecurityDescriptor(sd *byte, revision uint32, secInfo uint32, sddl **uint16, sddlSize *uint32) (err error) {
r1, _, e1 := syscall.Syscall6(procConvertSecurityDescriptorToStringSecurityDescriptorW.Addr(), 5, uintptr(unsafe.Pointer(sd)), uintptr(revision), uintptr(secInfo), uintptr(unsafe.Pointer(sddl)), uintptr(unsafe.Pointer(sddlSize)), 0)
if r1 == 0 {
if e1 != 0 {
err = error(e1)
} else {
err = syscall.EINVAL
}
}
return
}
func localFree(mem uintptr) {
syscall.Syscall(procLocalFree.Addr(), 1, uintptr(mem), 0, 0)
return
}
func getSecurityDescriptorLength(sd uintptr) (len uint32) {
r0, _, _ := syscall.Syscall(procGetSecurityDescriptorLength.Addr(), 1, uintptr(sd), 0, 0)
len = uint32(r0)
return
}
func getFileInformationByHandleEx(h syscall.Handle, class uint32, buffer *byte, size uint32) (err error) {
r1, _, e1 := syscall.Syscall6(procGetFileInformationByHandleEx.Addr(), 4, uintptr(h), uintptr(class), uintptr(unsafe.Pointer(buffer)), uintptr(size), 0, 0)
if r1 == 0 {
if e1 != 0 {
err = error(e1)
} else {
err = syscall.EINVAL
}
}
return
}
func setFileInformationByHandle(h syscall.Handle, class uint32, buffer *byte, size uint32) (err error) {
r1, _, e1 := syscall.Syscall6(procSetFileInformationByHandle.Addr(), 4, uintptr(h), uintptr(class), uintptr(unsafe.Pointer(buffer)), uintptr(size), 0, 0)
if r1 == 0 {
if e1 != 0 {
err = error(e1)
} else {
err = syscall.EINVAL
}
}
return
}
func adjustTokenPrivileges(token syscall.Handle, releaseAll bool, input *byte, outputSize uint32, output *byte, requiredSize *uint32) (success bool, err error) {
var _p0 uint32
if releaseAll {
_p0 = 1
} else {
_p0 = 0
}
r0, _, e1 := syscall.Syscall6(procAdjustTokenPrivileges.Addr(), 6, uintptr(token), uintptr(_p0), uintptr(unsafe.Pointer(input)), uintptr(outputSize), uintptr(unsafe.Pointer(output)), uintptr(unsafe.Pointer(requiredSize)))
success = r0 != 0
if true {
if e1 != 0 {
err = error(e1)
} else {
err = syscall.EINVAL
}
}
return
}
func impersonateSelf(level uint32) (err error) {
r1, _, e1 := syscall.Syscall(procImpersonateSelf.Addr(), 1, uintptr(level), 0, 0)
if r1 == 0 {
if e1 != 0 {
err = error(e1)
} else {
err = syscall.EINVAL
}
}
return
}
func revertToSelf() (err error) {
r1, _, e1 := syscall.Syscall(procRevertToSelf.Addr(), 0, 0, 0, 0)
if r1 == 0 {
if e1 != 0 {
err = error(e1)
} else {
err = syscall.EINVAL
}
}
return
}
func openThreadToken(thread syscall.Handle, accessMask uint32, openAsSelf bool, token *syscall.Handle) (err error) {
var _p0 uint32
if openAsSelf {
_p0 = 1
} else {
_p0 = 0
}
r1, _, e1 := syscall.Syscall6(procOpenThreadToken.Addr(), 4, uintptr(thread), uintptr(accessMask), uintptr(_p0), uintptr(unsafe.Pointer(token)), 0, 0)
if r1 == 0 {
if e1 != 0 {
err = error(e1)
} else {
err = syscall.EINVAL
}
}
return
}
func getCurrentThread() (h syscall.Handle) {
r0, _, _ := syscall.Syscall(procGetCurrentThread.Addr(), 0, 0, 0, 0)
h = syscall.Handle(r0)
return
}
func lookupPrivilegeValue(systemName string, name string, luid *uint64) (err error) {
var _p0 *uint16
_p0, err = syscall.UTF16PtrFromString(systemName)
if err != nil {
return
}
var _p1 *uint16
_p1, err = syscall.UTF16PtrFromString(name)
if err != nil {
return
}
return _lookupPrivilegeValue(_p0, _p1, luid)
}
func _lookupPrivilegeValue(systemName *uint16, name *uint16, luid *uint64) (err error) {
r1, _, e1 := syscall.Syscall(procLookupPrivilegeValueW.Addr(), 3, uintptr(unsafe.Pointer(systemName)), uintptr(unsafe.Pointer(name)), uintptr(unsafe.Pointer(luid)))
if r1 == 0 {
if e1 != 0 {
err = error(e1)
} else {
err = syscall.EINVAL
}
}
return
}
func lookupPrivilegeName(systemName string, luid *uint64, buffer *uint16, size *uint32) (err error) {
var _p0 *uint16
_p0, err = syscall.UTF16PtrFromString(systemName)
if err != nil {
return
}
return _lookupPrivilegeName(_p0, luid, buffer, size)
}
func _lookupPrivilegeName(systemName *uint16, luid *uint64, buffer *uint16, size *uint32) (err error) {
r1, _, e1 := syscall.Syscall6(procLookupPrivilegeNameW.Addr(), 4, uintptr(unsafe.Pointer(systemName)), uintptr(unsafe.Pointer(luid)), uintptr(unsafe.Pointer(buffer)), uintptr(unsafe.Pointer(size)), 0, 0)
if r1 == 0 {
if e1 != 0 {
err = error(e1)
} else {
err = syscall.EINVAL
}
}
return
}
func lookupPrivilegeDisplayName(systemName string, name *uint16, buffer *uint16, size *uint32, languageId *uint32) (err error) {
var _p0 *uint16
_p0, err = syscall.UTF16PtrFromString(systemName)
if err != nil {
return
}
return _lookupPrivilegeDisplayName(_p0, name, buffer, size, languageId)
}
func _lookupPrivilegeDisplayName(systemName *uint16, name *uint16, buffer *uint16, size *uint32, languageId *uint32) (err error) {
r1, _, e1 := syscall.Syscall6(procLookupPrivilegeDisplayNameW.Addr(), 5, uintptr(unsafe.Pointer(systemName)), uintptr(unsafe.Pointer(name)), uintptr(unsafe.Pointer(buffer)), uintptr(unsafe.Pointer(size)), uintptr(unsafe.Pointer(languageId)), 0)
if r1 == 0 {
if e1 != 0 {
err = error(e1)
} else {
err = syscall.EINVAL
}
}
return
}
func backupRead(h syscall.Handle, b []byte, bytesRead *uint32, abort bool, processSecurity bool, context *uintptr) (err error) {
var _p0 *byte
if len(b) > 0 {
_p0 = &b[0]
}
var _p1 uint32
if abort {
_p1 = 1
} else {
_p1 = 0
}
var _p2 uint32
if processSecurity {
_p2 = 1
} else {
_p2 = 0
}
r1, _, e1 := syscall.Syscall9(procBackupRead.Addr(), 7, uintptr(h), uintptr(unsafe.Pointer(_p0)), uintptr(len(b)), uintptr(unsafe.Pointer(bytesRead)), uintptr(_p1), uintptr(_p2), uintptr(unsafe.Pointer(context)), 0, 0)
if r1 == 0 {
if e1 != 0 {
err = error(e1)
} else {
err = syscall.EINVAL
}
}
return
}
func backupWrite(h syscall.Handle, b []byte, bytesWritten *uint32, abort bool, processSecurity bool, context *uintptr) (err error) {
var _p0 *byte
if len(b) > 0 {
_p0 = &b[0]
}
var _p1 uint32
if abort {
_p1 = 1
} else {
_p1 = 0
}
var _p2 uint32
if processSecurity {
_p2 = 1
} else {
_p2 = 0
}
r1, _, e1 := syscall.Syscall9(procBackupWrite.Addr(), 7, uintptr(h), uintptr(unsafe.Pointer(_p0)), uintptr(len(b)), uintptr(unsafe.Pointer(bytesWritten)), uintptr(_p1), uintptr(_p2), uintptr(unsafe.Pointer(context)), 0, 0)
if r1 == 0 {
if e1 != 0 {
err = error(e1)
} else {
err = syscall.EINVAL
}
}
return
}

View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2014 Simon Eskildsen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View file

@ -0,0 +1,26 @@
/*
Package logrus is a structured logger for Go, completely API compatible with the standard library logger.
The simplest way to use Logrus is simply the package-level exported logger:
package main
import (
log "github.com/Sirupsen/logrus"
)
func main() {
log.WithFields(log.Fields{
"animal": "walrus",
"number": 1,
"size": 10,
}).Info("A walrus appears")
}
Output:
time="2015-09-07T08:48:33Z" level=info msg="A walrus appears" animal=walrus number=1 size=10
For a full guide visit https://github.com/Sirupsen/logrus
*/
package logrus

View file

@ -0,0 +1,264 @@
package logrus
import (
"bytes"
"fmt"
"io"
"os"
"time"
)
// Defines the key when adding errors using WithError.
var ErrorKey = "error"
// An entry is the final or intermediate Logrus logging entry. It contains all
// the fields passed with WithField{,s}. It's finally logged when Debug, Info,
// Warn, Error, Fatal or Panic is called on it. These objects can be reused and
// passed around as much as you wish to avoid field duplication.
type Entry struct {
Logger *Logger
// Contains all the fields set by the user.
Data Fields
// Time at which the log entry was created
Time time.Time
// Level the log entry was logged at: Debug, Info, Warn, Error, Fatal or Panic
Level Level
// Message passed to Debug, Info, Warn, Error, Fatal or Panic
Message string
}
func NewEntry(logger *Logger) *Entry {
return &Entry{
Logger: logger,
// Default is three fields, give a little extra room
Data: make(Fields, 5),
}
}
// Returns a reader for the entry, which is a proxy to the formatter.
func (entry *Entry) Reader() (*bytes.Buffer, error) {
serialized, err := entry.Logger.Formatter.Format(entry)
return bytes.NewBuffer(serialized), err
}
// Returns the string representation from the reader and ultimately the
// formatter.
func (entry *Entry) String() (string, error) {
reader, err := entry.Reader()
if err != nil {
return "", err
}
return reader.String(), err
}
// Add an error as single field (using the key defined in ErrorKey) to the Entry.
func (entry *Entry) WithError(err error) *Entry {
return entry.WithField(ErrorKey, err)
}
// Add a single field to the Entry.
func (entry *Entry) WithField(key string, value interface{}) *Entry {
return entry.WithFields(Fields{key: value})
}
// Add a map of fields to the Entry.
func (entry *Entry) WithFields(fields Fields) *Entry {
data := make(Fields, len(entry.Data)+len(fields))
for k, v := range entry.Data {
data[k] = v
}
for k, v := range fields {
data[k] = v
}
return &Entry{Logger: entry.Logger, Data: data}
}
// This function is not declared with a pointer value because otherwise
// race conditions will occur when using multiple goroutines
func (entry Entry) log(level Level, msg string) {
entry.Time = time.Now()
entry.Level = level
entry.Message = msg
if err := entry.Logger.Hooks.Fire(level, &entry); err != nil {
entry.Logger.mu.Lock()
fmt.Fprintf(os.Stderr, "Failed to fire hook: %v\n", err)
entry.Logger.mu.Unlock()
}
reader, err := entry.Reader()
if err != nil {
entry.Logger.mu.Lock()
fmt.Fprintf(os.Stderr, "Failed to obtain reader, %v\n", err)
entry.Logger.mu.Unlock()
}
entry.Logger.mu.Lock()
defer entry.Logger.mu.Unlock()
_, err = io.Copy(entry.Logger.Out, reader)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to write to log, %v\n", err)
}
// To avoid Entry#log() returning a value that only would make sense for
// panic() to use in Entry#Panic(), we avoid the allocation by checking
// directly here.
if level <= PanicLevel {
panic(&entry)
}
}
func (entry *Entry) Debug(args ...interface{}) {
if entry.Logger.Level >= DebugLevel {
entry.log(DebugLevel, fmt.Sprint(args...))
}
}
func (entry *Entry) Print(args ...interface{}) {
entry.Info(args...)
}
func (entry *Entry) Info(args ...interface{}) {
if entry.Logger.Level >= InfoLevel {
entry.log(InfoLevel, fmt.Sprint(args...))
}
}
func (entry *Entry) Warn(args ...interface{}) {
if entry.Logger.Level >= WarnLevel {
entry.log(WarnLevel, fmt.Sprint(args...))
}
}
func (entry *Entry) Warning(args ...interface{}) {
entry.Warn(args...)
}
func (entry *Entry) Error(args ...interface{}) {
if entry.Logger.Level >= ErrorLevel {
entry.log(ErrorLevel, fmt.Sprint(args...))
}
}
func (entry *Entry) Fatal(args ...interface{}) {
if entry.Logger.Level >= FatalLevel {
entry.log(FatalLevel, fmt.Sprint(args...))
}
os.Exit(1)
}
func (entry *Entry) Panic(args ...interface{}) {
if entry.Logger.Level >= PanicLevel {
entry.log(PanicLevel, fmt.Sprint(args...))
}
panic(fmt.Sprint(args...))
}
// Entry Printf family functions
func (entry *Entry) Debugf(format string, args ...interface{}) {
if entry.Logger.Level >= DebugLevel {
entry.Debug(fmt.Sprintf(format, args...))
}
}
func (entry *Entry) Infof(format string, args ...interface{}) {
if entry.Logger.Level >= InfoLevel {
entry.Info(fmt.Sprintf(format, args...))
}
}
func (entry *Entry) Printf(format string, args ...interface{}) {
entry.Infof(format, args...)
}
func (entry *Entry) Warnf(format string, args ...interface{}) {
if entry.Logger.Level >= WarnLevel {
entry.Warn(fmt.Sprintf(format, args...))
}
}
func (entry *Entry) Warningf(format string, args ...interface{}) {
entry.Warnf(format, args...)
}
func (entry *Entry) Errorf(format string, args ...interface{}) {
if entry.Logger.Level >= ErrorLevel {
entry.Error(fmt.Sprintf(format, args...))
}
}
func (entry *Entry) Fatalf(format string, args ...interface{}) {
if entry.Logger.Level >= FatalLevel {
entry.Fatal(fmt.Sprintf(format, args...))
}
os.Exit(1)
}
func (entry *Entry) Panicf(format string, args ...interface{}) {
if entry.Logger.Level >= PanicLevel {
entry.Panic(fmt.Sprintf(format, args...))
}
}
// Entry Println family functions
func (entry *Entry) Debugln(args ...interface{}) {
if entry.Logger.Level >= DebugLevel {
entry.Debug(entry.sprintlnn(args...))
}
}
func (entry *Entry) Infoln(args ...interface{}) {
if entry.Logger.Level >= InfoLevel {
entry.Info(entry.sprintlnn(args...))
}
}
func (entry *Entry) Println(args ...interface{}) {
entry.Infoln(args...)
}
func (entry *Entry) Warnln(args ...interface{}) {
if entry.Logger.Level >= WarnLevel {
entry.Warn(entry.sprintlnn(args...))
}
}
func (entry *Entry) Warningln(args ...interface{}) {
entry.Warnln(args...)
}
func (entry *Entry) Errorln(args ...interface{}) {
if entry.Logger.Level >= ErrorLevel {
entry.Error(entry.sprintlnn(args...))
}
}
func (entry *Entry) Fatalln(args ...interface{}) {
if entry.Logger.Level >= FatalLevel {
entry.Fatal(entry.sprintlnn(args...))
}
os.Exit(1)
}
func (entry *Entry) Panicln(args ...interface{}) {
if entry.Logger.Level >= PanicLevel {
entry.Panic(entry.sprintlnn(args...))
}
}
// Sprintlnn => Sprint no newline. This is to get the behavior of how
// fmt.Sprintln where spaces are always added between operands, regardless of
// their type. Instead of vendoring the Sprintln implementation to spare a
// string allocation, we do the simplest thing.
func (entry *Entry) sprintlnn(args ...interface{}) string {
msg := fmt.Sprintln(args...)
return msg[:len(msg)-1]
}

View file

@ -0,0 +1,193 @@
package logrus
import (
"io"
)
var (
// std is the name of the standard logger in stdlib `log`
std = New()
)
func StandardLogger() *Logger {
return std
}
// SetOutput sets the standard logger output.
func SetOutput(out io.Writer) {
std.mu.Lock()
defer std.mu.Unlock()
std.Out = out
}
// SetFormatter sets the standard logger formatter.
func SetFormatter(formatter Formatter) {
std.mu.Lock()
defer std.mu.Unlock()
std.Formatter = formatter
}
// SetLevel sets the standard logger level.
func SetLevel(level Level) {
std.mu.Lock()
defer std.mu.Unlock()
std.Level = level
}
// GetLevel returns the standard logger level.
func GetLevel() Level {
std.mu.Lock()
defer std.mu.Unlock()
return std.Level
}
// AddHook adds a hook to the standard logger hooks.
func AddHook(hook Hook) {
std.mu.Lock()
defer std.mu.Unlock()
std.Hooks.Add(hook)
}
// WithError creates an entry from the standard logger and adds an error to it, using the value defined in ErrorKey as key.
func WithError(err error) *Entry {
return std.WithField(ErrorKey, err)
}
// WithField creates an entry from the standard logger and adds a field to
// it. If you want multiple fields, use `WithFields`.
//
// Note that it doesn't log until you call Debug, Print, Info, Warn, Fatal
// or Panic on the Entry it returns.
func WithField(key string, value interface{}) *Entry {
return std.WithField(key, value)
}
// WithFields creates an entry from the standard logger and adds multiple
// fields to it. This is simply a helper for `WithField`, invoking it
// once for each field.
//
// Note that it doesn't log until you call Debug, Print, Info, Warn, Fatal
// or Panic on the Entry it returns.
func WithFields(fields Fields) *Entry {
return std.WithFields(fields)
}
// Debug logs a message at level Debug on the standard logger.
func Debug(args ...interface{}) {
std.Debug(args...)
}
// Print logs a message at level Info on the standard logger.
func Print(args ...interface{}) {
std.Print(args...)
}
// Info logs a message at level Info on the standard logger.
func Info(args ...interface{}) {
std.Info(args...)
}
// Warn logs a message at level Warn on the standard logger.
func Warn(args ...interface{}) {
std.Warn(args...)
}
// Warning logs a message at level Warn on the standard logger.
func Warning(args ...interface{}) {
std.Warning(args...)
}
// Error logs a message at level Error on the standard logger.
func Error(args ...interface{}) {
std.Error(args...)
}
// Panic logs a message at level Panic on the standard logger.
func Panic(args ...interface{}) {
std.Panic(args...)
}
// Fatal logs a message at level Fatal on the standard logger.
func Fatal(args ...interface{}) {
std.Fatal(args...)
}
// Debugf logs a message at level Debug on the standard logger.
func Debugf(format string, args ...interface{}) {
std.Debugf(format, args...)
}
// Printf logs a message at level Info on the standard logger.
func Printf(format string, args ...interface{}) {
std.Printf(format, args...)
}
// Infof logs a message at level Info on the standard logger.
func Infof(format string, args ...interface{}) {
std.Infof(format, args...)
}
// Warnf logs a message at level Warn on the standard logger.
func Warnf(format string, args ...interface{}) {
std.Warnf(format, args...)
}
// Warningf logs a message at level Warn on the standard logger.
func Warningf(format string, args ...interface{}) {
std.Warningf(format, args...)
}
// Errorf logs a message at level Error on the standard logger.
func Errorf(format string, args ...interface{}) {
std.Errorf(format, args...)
}
// Panicf logs a message at level Panic on the standard logger.
func Panicf(format string, args ...interface{}) {
std.Panicf(format, args...)
}
// Fatalf logs a message at level Fatal on the standard logger.
func Fatalf(format string, args ...interface{}) {
std.Fatalf(format, args...)
}
// Debugln logs a message at level Debug on the standard logger.
func Debugln(args ...interface{}) {
std.Debugln(args...)
}
// Println logs a message at level Info on the standard logger.
func Println(args ...interface{}) {
std.Println(args...)
}
// Infoln logs a message at level Info on the standard logger.
func Infoln(args ...interface{}) {
std.Infoln(args...)
}
// Warnln logs a message at level Warn on the standard logger.
func Warnln(args ...interface{}) {
std.Warnln(args...)
}
// Warningln logs a message at level Warn on the standard logger.
func Warningln(args ...interface{}) {
std.Warningln(args...)
}
// Errorln logs a message at level Error on the standard logger.
func Errorln(args ...interface{}) {
std.Errorln(args...)
}
// Panicln logs a message at level Panic on the standard logger.
func Panicln(args ...interface{}) {
std.Panicln(args...)
}
// Fatalln logs a message at level Fatal on the standard logger.
func Fatalln(args ...interface{}) {
std.Fatalln(args...)
}

View file

@ -0,0 +1,48 @@
package logrus
import "time"
const DefaultTimestampFormat = time.RFC3339
// The Formatter interface is used to implement a custom Formatter. It takes an
// `Entry`. It exposes all the fields, including the default ones:
//
// * `entry.Data["msg"]`. The message passed from Info, Warn, Error ..
// * `entry.Data["time"]`. The timestamp.
// * `entry.Data["level"]. The level the entry was logged at.
//
// Any additional fields added with `WithField` or `WithFields` are also in
// `entry.Data`. Format is expected to return an array of bytes which are then
// logged to `logger.Out`.
type Formatter interface {
Format(*Entry) ([]byte, error)
}
// This is to not silently overwrite `time`, `msg` and `level` fields when
// dumping it. If this code wasn't there doing:
//
// logrus.WithField("level", 1).Info("hello")
//
// Would just silently drop the user provided level. Instead with this code
// it'll logged as:
//
// {"level": "info", "fields.level": 1, "msg": "hello", "time": "..."}
//
// It's not exported because it's still using Data in an opinionated way. It's to
// avoid code duplication between the two default formatters.
func prefixFieldClashes(data Fields) {
_, ok := data["time"]
if ok {
data["fields.time"] = data["time"]
}
_, ok = data["msg"]
if ok {
data["fields.msg"] = data["msg"]
}
_, ok = data["level"]
if ok {
data["fields.level"] = data["level"]
}
}

View file

@ -0,0 +1,34 @@
package logrus
// A hook to be fired when logging on the logging levels returned from
// `Levels()` on your implementation of the interface. Note that this is not
// fired in a goroutine or a channel with workers, you should handle such
// functionality yourself if your call is non-blocking and you don't wish for
// the logging calls for levels returned from `Levels()` to block.
type Hook interface {
Levels() []Level
Fire(*Entry) error
}
// Internal type for storing the hooks on a logger instance.
type LevelHooks map[Level][]Hook
// Add a hook to an instance of logger. This is called with
// `log.Hooks.Add(new(MyHook))` where `MyHook` implements the `Hook` interface.
func (hooks LevelHooks) Add(hook Hook) {
for _, level := range hook.Levels() {
hooks[level] = append(hooks[level], hook)
}
}
// Fire all the hooks for the passed level. Used by `entry.log` to fire
// appropriate hooks for a log entry.
func (hooks LevelHooks) Fire(level Level, entry *Entry) error {
for _, hook := range hooks[level] {
if err := hook.Fire(entry); err != nil {
return err
}
}
return nil
}

View file

@ -0,0 +1,41 @@
package logrus
import (
"encoding/json"
"fmt"
)
type JSONFormatter struct {
// TimestampFormat sets the format used for marshaling timestamps.
TimestampFormat string
}
func (f *JSONFormatter) Format(entry *Entry) ([]byte, error) {
data := make(Fields, len(entry.Data)+3)
for k, v := range entry.Data {
switch v := v.(type) {
case error:
// Otherwise errors are ignored by `encoding/json`
// https://github.com/Sirupsen/logrus/issues/137
data[k] = v.Error()
default:
data[k] = v
}
}
prefixFieldClashes(data)
timestampFormat := f.TimestampFormat
if timestampFormat == "" {
timestampFormat = DefaultTimestampFormat
}
data["time"] = entry.Time.Format(timestampFormat)
data["msg"] = entry.Message
data["level"] = entry.Level.String()
serialized, err := json.Marshal(data)
if err != nil {
return nil, fmt.Errorf("Failed to marshal fields to JSON, %v", err)
}
return append(serialized, '\n'), nil
}

View file

@ -0,0 +1,212 @@
package logrus
import (
"io"
"os"
"sync"
)
type Logger struct {
// The logs are `io.Copy`'d to this in a mutex. It's common to set this to a
// file, or leave it default which is `os.Stderr`. You can also set this to
// something more adventorous, such as logging to Kafka.
Out io.Writer
// Hooks for the logger instance. These allow firing events based on logging
// levels and log entries. For example, to send errors to an error tracking
// service, log to StatsD or dump the core on fatal errors.
Hooks LevelHooks
// All log entries pass through the formatter before logged to Out. The
// included formatters are `TextFormatter` and `JSONFormatter` for which
// TextFormatter is the default. In development (when a TTY is attached) it
// logs with colors, but to a file it wouldn't. You can easily implement your
// own that implements the `Formatter` interface, see the `README` or included
// formatters for examples.
Formatter Formatter
// The logging level the logger should log at. This is typically (and defaults
// to) `logrus.Info`, which allows Info(), Warn(), Error() and Fatal() to be
// logged. `logrus.Debug` is useful in
Level Level
// Used to sync writing to the log.
mu sync.Mutex
}
// Creates a new logger. Configuration should be set by changing `Formatter`,
// `Out` and `Hooks` directly on the default logger instance. You can also just
// instantiate your own:
//
// var log = &Logger{
// Out: os.Stderr,
// Formatter: new(JSONFormatter),
// Hooks: make(LevelHooks),
// Level: logrus.DebugLevel,
// }
//
// It's recommended to make this a global instance called `log`.
func New() *Logger {
return &Logger{
Out: os.Stderr,
Formatter: new(TextFormatter),
Hooks: make(LevelHooks),
Level: InfoLevel,
}
}
// Adds a field to the log entry, note that you it doesn't log until you call
// Debug, Print, Info, Warn, Fatal or Panic. It only creates a log entry.
// If you want multiple fields, use `WithFields`.
func (logger *Logger) WithField(key string, value interface{}) *Entry {
return NewEntry(logger).WithField(key, value)
}
// Adds a struct of fields to the log entry. All it does is call `WithField` for
// each `Field`.
func (logger *Logger) WithFields(fields Fields) *Entry {
return NewEntry(logger).WithFields(fields)
}
// Add an error as single field to the log entry. All it does is call
// `WithError` for the given `error`.
func (logger *Logger) WithError(err error) *Entry {
return NewEntry(logger).WithError(err)
}
func (logger *Logger) Debugf(format string, args ...interface{}) {
if logger.Level >= DebugLevel {
NewEntry(logger).Debugf(format, args...)
}
}
func (logger *Logger) Infof(format string, args ...interface{}) {
if logger.Level >= InfoLevel {
NewEntry(logger).Infof(format, args...)
}
}
func (logger *Logger) Printf(format string, args ...interface{}) {
NewEntry(logger).Printf(format, args...)
}
func (logger *Logger) Warnf(format string, args ...interface{}) {
if logger.Level >= WarnLevel {
NewEntry(logger).Warnf(format, args...)
}
}
func (logger *Logger) Warningf(format string, args ...interface{}) {
if logger.Level >= WarnLevel {
NewEntry(logger).Warnf(format, args...)
}
}
func (logger *Logger) Errorf(format string, args ...interface{}) {
if logger.Level >= ErrorLevel {
NewEntry(logger).Errorf(format, args...)
}
}
func (logger *Logger) Fatalf(format string, args ...interface{}) {
if logger.Level >= FatalLevel {
NewEntry(logger).Fatalf(format, args...)
}
os.Exit(1)
}
func (logger *Logger) Panicf(format string, args ...interface{}) {
if logger.Level >= PanicLevel {
NewEntry(logger).Panicf(format, args...)
}
}
func (logger *Logger) Debug(args ...interface{}) {
if logger.Level >= DebugLevel {
NewEntry(logger).Debug(args...)
}
}
func (logger *Logger) Info(args ...interface{}) {
if logger.Level >= InfoLevel {
NewEntry(logger).Info(args...)
}
}
func (logger *Logger) Print(args ...interface{}) {
NewEntry(logger).Info(args...)
}
func (logger *Logger) Warn(args ...interface{}) {
if logger.Level >= WarnLevel {
NewEntry(logger).Warn(args...)
}
}
func (logger *Logger) Warning(args ...interface{}) {
if logger.Level >= WarnLevel {
NewEntry(logger).Warn(args...)
}
}
func (logger *Logger) Error(args ...interface{}) {
if logger.Level >= ErrorLevel {
NewEntry(logger).Error(args...)
}
}
func (logger *Logger) Fatal(args ...interface{}) {
if logger.Level >= FatalLevel {
NewEntry(logger).Fatal(args...)
}
os.Exit(1)
}
func (logger *Logger) Panic(args ...interface{}) {
if logger.Level >= PanicLevel {
NewEntry(logger).Panic(args...)
}
}
func (logger *Logger) Debugln(args ...interface{}) {
if logger.Level >= DebugLevel {
NewEntry(logger).Debugln(args...)
}
}
func (logger *Logger) Infoln(args ...interface{}) {
if logger.Level >= InfoLevel {
NewEntry(logger).Infoln(args...)
}
}
func (logger *Logger) Println(args ...interface{}) {
NewEntry(logger).Println(args...)
}
func (logger *Logger) Warnln(args ...interface{}) {
if logger.Level >= WarnLevel {
NewEntry(logger).Warnln(args...)
}
}
func (logger *Logger) Warningln(args ...interface{}) {
if logger.Level >= WarnLevel {
NewEntry(logger).Warnln(args...)
}
}
func (logger *Logger) Errorln(args ...interface{}) {
if logger.Level >= ErrorLevel {
NewEntry(logger).Errorln(args...)
}
}
func (logger *Logger) Fatalln(args ...interface{}) {
if logger.Level >= FatalLevel {
NewEntry(logger).Fatalln(args...)
}
os.Exit(1)
}
func (logger *Logger) Panicln(args ...interface{}) {
if logger.Level >= PanicLevel {
NewEntry(logger).Panicln(args...)
}
}

View file

@ -0,0 +1,143 @@
package logrus
import (
"fmt"
"log"
"strings"
)
// Fields type, used to pass to `WithFields`.
type Fields map[string]interface{}
// Level type
type Level uint8
// Convert the Level to a string. E.g. PanicLevel becomes "panic".
func (level Level) String() string {
switch level {
case DebugLevel:
return "debug"
case InfoLevel:
return "info"
case WarnLevel:
return "warning"
case ErrorLevel:
return "error"
case FatalLevel:
return "fatal"
case PanicLevel:
return "panic"
}
return "unknown"
}
// ParseLevel takes a string level and returns the Logrus log level constant.
func ParseLevel(lvl string) (Level, error) {
switch strings.ToLower(lvl) {
case "panic":
return PanicLevel, nil
case "fatal":
return FatalLevel, nil
case "error":
return ErrorLevel, nil
case "warn", "warning":
return WarnLevel, nil
case "info":
return InfoLevel, nil
case "debug":
return DebugLevel, nil
}
var l Level
return l, fmt.Errorf("not a valid logrus Level: %q", lvl)
}
// A constant exposing all logging levels
var AllLevels = []Level{
PanicLevel,
FatalLevel,
ErrorLevel,
WarnLevel,
InfoLevel,
DebugLevel,
}
// These are the different logging levels. You can set the logging level to log
// on your instance of logger, obtained with `logrus.New()`.
const (
// PanicLevel level, highest level of severity. Logs and then calls panic with the
// message passed to Debug, Info, ...
PanicLevel Level = iota
// FatalLevel level. Logs and then calls `os.Exit(1)`. It will exit even if the
// logging level is set to Panic.
FatalLevel
// ErrorLevel level. Logs. Used for errors that should definitely be noted.
// Commonly used for hooks to send errors to an error tracking service.
ErrorLevel
// WarnLevel level. Non-critical entries that deserve eyes.
WarnLevel
// InfoLevel level. General operational entries about what's going on inside the
// application.
InfoLevel
// DebugLevel level. Usually only enabled when debugging. Very verbose logging.
DebugLevel
)
// Won't compile if StdLogger can't be realized by a log.Logger
var (
_ StdLogger = &log.Logger{}
_ StdLogger = &Entry{}
_ StdLogger = &Logger{}
)
// StdLogger is what your logrus-enabled library should take, that way
// it'll accept a stdlib logger and a logrus logger. There's no standard
// interface, this is the closest we get, unfortunately.
type StdLogger interface {
Print(...interface{})
Printf(string, ...interface{})
Println(...interface{})
Fatal(...interface{})
Fatalf(string, ...interface{})
Fatalln(...interface{})
Panic(...interface{})
Panicf(string, ...interface{})
Panicln(...interface{})
}
// The FieldLogger interface generalizes the Entry and Logger types
type FieldLogger interface {
WithField(key string, value interface{}) *Entry
WithFields(fields Fields) *Entry
WithError(err error) *Entry
Debugf(format string, args ...interface{})
Infof(format string, args ...interface{})
Printf(format string, args ...interface{})
Warnf(format string, args ...interface{})
Warningf(format string, args ...interface{})
Errorf(format string, args ...interface{})
Fatalf(format string, args ...interface{})
Panicf(format string, args ...interface{})
Debug(args ...interface{})
Info(args ...interface{})
Print(args ...interface{})
Warn(args ...interface{})
Warning(args ...interface{})
Error(args ...interface{})
Fatal(args ...interface{})
Panic(args ...interface{})
Debugln(args ...interface{})
Infoln(args ...interface{})
Println(args ...interface{})
Warnln(args ...interface{})
Warningln(args ...interface{})
Errorln(args ...interface{})
Fatalln(args ...interface{})
Panicln(args ...interface{})
}

View file

@ -0,0 +1,9 @@
// +build darwin freebsd openbsd netbsd dragonfly
package logrus
import "syscall"
const ioctlReadTermios = syscall.TIOCGETA
type Termios syscall.Termios

View file

@ -0,0 +1,12 @@
// Based on ssh/terminal:
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package logrus
import "syscall"
const ioctlReadTermios = syscall.TCGETS
type Termios syscall.Termios

View file

@ -0,0 +1,21 @@
// Based on ssh/terminal:
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build linux darwin freebsd openbsd netbsd dragonfly
package logrus
import (
"syscall"
"unsafe"
)
// IsTerminal returns true if stderr's file descriptor is a terminal.
func IsTerminal() bool {
fd := syscall.Stderr
var termios Termios
_, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlReadTermios, uintptr(unsafe.Pointer(&termios)), 0, 0, 0)
return err == 0
}

View file

@ -0,0 +1,15 @@
// +build solaris
package logrus
import (
"os"
"golang.org/x/sys/unix"
)
// IsTerminal returns true if the given file descriptor is a terminal.
func IsTerminal() bool {
_, err := unix.IoctlGetTermios(int(os.Stdout.Fd()), unix.TCGETA)
return err == nil
}

View file

@ -0,0 +1,27 @@
// Based on ssh/terminal:
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build windows
package logrus
import (
"syscall"
"unsafe"
)
var kernel32 = syscall.NewLazyDLL("kernel32.dll")
var (
procGetConsoleMode = kernel32.NewProc("GetConsoleMode")
)
// IsTerminal returns true if stderr's file descriptor is a terminal.
func IsTerminal() bool {
fd := syscall.Stderr
var st uint32
r, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&st)), 0)
return r != 0 && e == 0
}

View file

@ -0,0 +1,161 @@
package logrus
import (
"bytes"
"fmt"
"runtime"
"sort"
"strings"
"time"
)
const (
nocolor = 0
red = 31
green = 32
yellow = 33
blue = 34
gray = 37
)
var (
baseTimestamp time.Time
isTerminal bool
)
func init() {
baseTimestamp = time.Now()
isTerminal = IsTerminal()
}
func miniTS() int {
return int(time.Since(baseTimestamp) / time.Second)
}
type TextFormatter struct {
// Set to true to bypass checking for a TTY before outputting colors.
ForceColors bool
// Force disabling colors.
DisableColors bool
// Disable timestamp logging. useful when output is redirected to logging
// system that already adds timestamps.
DisableTimestamp bool
// Enable logging the full timestamp when a TTY is attached instead of just
// the time passed since beginning of execution.
FullTimestamp bool
// TimestampFormat to use for display when a full timestamp is printed
TimestampFormat string
// The fields are sorted by default for a consistent output. For applications
// that log extremely frequently and don't use the JSON formatter this may not
// be desired.
DisableSorting bool
}
func (f *TextFormatter) Format(entry *Entry) ([]byte, error) {
var keys []string = make([]string, 0, len(entry.Data))
for k := range entry.Data {
keys = append(keys, k)
}
if !f.DisableSorting {
sort.Strings(keys)
}
b := &bytes.Buffer{}
prefixFieldClashes(entry.Data)
isColorTerminal := isTerminal && (runtime.GOOS != "windows")
isColored := (f.ForceColors || isColorTerminal) && !f.DisableColors
timestampFormat := f.TimestampFormat
if timestampFormat == "" {
timestampFormat = DefaultTimestampFormat
}
if isColored {
f.printColored(b, entry, keys, timestampFormat)
} else {
if !f.DisableTimestamp {
f.appendKeyValue(b, "time", entry.Time.Format(timestampFormat))
}
f.appendKeyValue(b, "level", entry.Level.String())
if entry.Message != "" {
f.appendKeyValue(b, "msg", entry.Message)
}
for _, key := range keys {
f.appendKeyValue(b, key, entry.Data[key])
}
}
b.WriteByte('\n')
return b.Bytes(), nil
}
func (f *TextFormatter) printColored(b *bytes.Buffer, entry *Entry, keys []string, timestampFormat string) {
var levelColor int
switch entry.Level {
case DebugLevel:
levelColor = gray
case WarnLevel:
levelColor = yellow
case ErrorLevel, FatalLevel, PanicLevel:
levelColor = red
default:
levelColor = blue
}
levelText := strings.ToUpper(entry.Level.String())[0:4]
if !f.FullTimestamp {
fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%04d] %-44s ", levelColor, levelText, miniTS(), entry.Message)
} else {
fmt.Fprintf(b, "\x1b[%dm%s\x1b[0m[%s] %-44s ", levelColor, levelText, entry.Time.Format(timestampFormat), entry.Message)
}
for _, k := range keys {
v := entry.Data[k]
fmt.Fprintf(b, " \x1b[%dm%s\x1b[0m=%+v", levelColor, k, v)
}
}
func needsQuoting(text string) bool {
for _, ch := range text {
if !((ch >= 'a' && ch <= 'z') ||
(ch >= 'A' && ch <= 'Z') ||
(ch >= '0' && ch <= '9') ||
ch == '-' || ch == '.') {
return false
}
}
return true
}
func (f *TextFormatter) appendKeyValue(b *bytes.Buffer, key string, value interface{}) {
b.WriteString(key)
b.WriteByte('=')
switch value := value.(type) {
case string:
if needsQuoting(value) {
b.WriteString(value)
} else {
fmt.Fprintf(b, "%q", value)
}
case error:
errmsg := value.Error()
if needsQuoting(errmsg) {
b.WriteString(errmsg)
} else {
fmt.Fprintf(b, "%q", value)
}
default:
fmt.Fprint(b, value)
}
b.WriteByte(' ')
}

View file

@ -0,0 +1,31 @@
package logrus
import (
"bufio"
"io"
"runtime"
)
func (logger *Logger) Writer() *io.PipeWriter {
reader, writer := io.Pipe()
go logger.writerScanner(reader)
runtime.SetFinalizer(writer, writerFinalizer)
return writer
}
func (logger *Logger) writerScanner(reader *io.PipeReader) {
scanner := bufio.NewScanner(reader)
for scanner.Scan() {
logger.Print(scanner.Text())
}
if err := scanner.Err(); err != nil {
logger.Errorf("Error while reading from Writer: %s", err)
}
reader.Close()
}
func writerFinalizer(writer *io.PipeWriter) {
writer.Close()
}

Some files were not shown because too many files have changed in this diff Show more