<commit_before>// Copyright 2012, Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package schema

// Yes, this sucks. It's a tiny tiny package that needs to be on its own
// It contains a data structure that's shared between sqlparser & tabletserver

import (
	"strings"

	log "github.com/ngaut/logging"

	"github.com/wandoulabs/cm/mysql"
)

// Cache types
const (
	CACHE_NONE = 0
	CACHE_RW   = 1
	CACHE_W    = 2
)

type TableColumn struct {
	Name     string
	Category byte
	IsAuto   bool
	Default  mysql.Value
}

type Table struct {
	Name      string
	Columns   []TableColumn
	Indexes   []*Index
	PKColumns []int
	CacheType int
}

func NewTable(name string) *Table {
	return &Table{
		Name:    name,
		Columns: make([]TableColumn, 0, 16),
		Indexes: make([]*Index, 0, 8),
	}
}

var typesMap = map[string]byte{
	"int": mysql.MYSQL_TYPE_LONG,
}

func init() {

}

func str2mysqlType(columnType string) byte {
	b, ok := typesMap[columnType]
	if !ok {
		log.Fatalf("%s not exist", columnType)
	}

	return b
}

func (ta *Table) AddColumn(name string, columnType string, defval mysql.Value, extra string) {
	index := len(ta.Columns)
	ta.Columns = append(ta.Columns, TableColumn{Name: name})
	if strings.Contains(columnType, "int") || strings.Contains(columnType, "long") || strings.Contains(columnType, "tiny") || strings.Contains(columnType, "short") {
		ta.Columns[index].Category = mysql.MYSQL_TYPE_LONGLONG
	} else if strings.HasPrefix(columnType, "varbinary") || strings.Contains(columnType, "blob") {
		ta.Columns[index].Category = mysql.MYSQL_TYPE_VARCHAR
	} else if strings.HasPrefix(columnType, "datetime") || strings.HasPrefix(columnType, "timestamp") {
		ta.Columns[index].Category = mysql.MYSQL_TYPE_DATETIME
	} else if strings.HasPrefix(columnType, "date") {
		ta.Columns[index].Category = mysql.MYSQL_TYPE_DATE
	} else if strings.Contains(columnType, "float") || strings.Contains(columnType, "double") {
		ta.Columns[index].Category = mysql.MYSQL_TYPE_DOUBLE
	} else if strings.HasPrefix(columnType, "enum") {
		ta.Columns[index].Category = mysql.MYSQL_TYPE_ENUM
	} else if strings.Contains(columnType, "text") || strings.Contains(columnType, "varchar") || strings.Contains(columnType, "string") || strings.Contains(columnType, "char") {
		ta.Columns[index].Category = mysql.MYSQL_TYPE_STRING
	} else {
		log.Fatalf("not support type: %s", columnType)
	}
	if extra == "auto_increment" {
		ta.Columns[index].IsAuto = true
		// Ignore default value, if any
		return
	}
	if defval == nil {
		return
	}
	ta.Columns[index].Default = defval
}

func (ta *Table) FindColumn(name string) int {
	for i, col := range ta.Columns {
		if col.Name == name {
			return i
		}
	}

	return -1
}

func (ta *Table) GetPKColumn(index int) *TableColumn {
	return &ta.Columns[ta.PKColumns[index]]
}

func (ta *Table) AddIndex(name string) (index *Index) {
	index = NewIndex(name)
	ta.Indexes = append(ta.Indexes, index)

	return index
}

type Index struct {
	Name        string
	Columns     []string
	Cardinality []uint64
	DataColumns []string
}

func NewIndex(name string) *Index {
	return &Index{name, make([]string, 0, 8), make([]uint64, 0, 8), nil}
}

func (idx *Index) AddColumn(name string, cardinality uint64) {
	idx.Columns = append(idx.Columns, name)
	if cardinality == 0 {
		cardinality = uint64(len(idx.Cardinality) + 1)
	}
	idx.Cardinality = append(idx.Cardinality, cardinality)
}

func (idx *Index) FindColumn(name string) int {
	for i, colName := range idx.Columns {
		if name == colName {
			return i
		}
	}

	return -1
}

func (idx *Index) FindDataColumn(name string) int {
	for i, colName := range idx.DataColumns {
		if name == colName {
			return i
		}
	}

	return -1
}
<commit_msg>more mysql types<commit_after>// Copyright 2012, Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package schema

// Yes, this sucks. It's a tiny tiny package that needs to be on its own
// It contains a data structure that's shared between sqlparser & tabletserver

import (
	"strings"

	log "github.com/ngaut/logging"

	"github.com/wandoulabs/cm/mysql"
)

// Cache types
const (
	CACHE_NONE = 0
	CACHE_RW   = 1
	CACHE_W    = 2
)

type TableColumn struct {
	Name     string
	Category byte
	IsAuto   bool
	Default  mysql.Value
}

type Table struct {
	Name      string
	Columns   []TableColumn
	Indexes   []*Index
	PKColumns []int
	CacheType int
}

func NewTable(name string) *Table {
	return &Table{
		Name:    name,
		Columns: make([]TableColumn, 0, 16),
		Indexes: make([]*Index, 0, 8),
	}
}

var typesMap = map[string]byte{
	"int":       mysql.MYSQL_TYPE_LONG,
	"long":      mysql.MYSQL_TYPE_LONG,
	"tiny":      mysql.MYSQL_TYPE_TINY,
	"varbinary": mysql.MYSQL_TYPE_VARCHAR,
	"blob":      mysql.MYSQL_TYPE_BLOB,
	"datatime":  mysql.MYSQL_TYPE_DATETIME,
	"timestamp": mysql.MYSQL_TYPE_TIMESTAMP,
	"data":      mysql.MYSQL_TYPE_DATE,
	"float":     mysql.MYSQL_TYPE_FLOAT,
	"enum":      mysql.MYSQL_TYPE_ENUM,
	"text":      mysql.MYSQL_TYPE_STRING,
	"varchar":   mysql.MYSQL_TYPE_VARCHAR,
	"string":    mysql.MYSQL_TYPE_STRING,
	"char":      mysql.MYSQL_TYPE_STRING,
}

func init() {

}

func str2mysqlType(columnType string) byte {
	b, ok := typesMap[columnType]
	if !ok {
		log.Fatalf("%s not exist", columnType)
	}

	return b
}

func (ta *Table) AddColumn(name string, columnType string, defval mysql.Value, extra string) {
	index := len(ta.Columns)
	ta.Columns = append(ta.Columns, TableColumn{Name: name})
	if strings.Contains(columnType, "int") || strings.Contains(columnType, "long") || strings.Contains(columnType, "tiny") || strings.Contains(columnType, "short") {
		ta.Columns[index].Category = mysql.MYSQL_TYPE_LONGLONG
	} else if strings.HasPrefix(columnType, "varbinary") || strings.Contains(columnType, "blob") {
		ta.Columns[index].Category = mysql.MYSQL_TYPE_VARCHAR
	} else if strings.HasPrefix(columnType, "datetime") || strings.HasPrefix(columnType, "timestamp") {
		ta.Columns[index].Category = mysql.MYSQL_TYPE_DATETIME
	} else if strings.HasPrefix(columnType, "date") {
		ta.Columns[index].Category = mysql.MYSQL_TYPE_DATE
	} else if strings.Contains(columnType, "float") || strings.Contains(columnType, "double") {
		ta.Columns[index].Category = mysql.MYSQL_TYPE_DOUBLE
	} else if strings.HasPrefix(columnType, "enum") {
		ta.Columns[index].Category = mysql.MYSQL_TYPE_ENUM
	} else if strings.Contains(columnType, "text") || strings.Contains(columnType, "varchar") || strings.Contains(columnType, "string") || strings.Contains(columnType, "char") {
		ta.Columns[index].Category = mysql.MYSQL_TYPE_STRING
	} else {
		log.Fatalf("not support type: %s", columnType)
	}
	if extra == "auto_increment" {
		ta.Columns[index].IsAuto = true
		// Ignore default value, if any
		return
	}
	if defval == nil {
		return
	}
	ta.Columns[index].Default = defval
}

func (ta *Table) FindColumn(name string) int {
	for i, col := range ta.Columns {
		if col.Name == name {
			return i
		}
	}

	return -1
}

func (ta *Table) GetPKColumn(index int) *TableColumn {
	return &ta.Columns[ta.PKColumns[index]]
}

func (ta *Table) AddIndex(name string) (index *Index) {
	index = NewIndex(name)
	ta.Indexes = append(ta.Indexes, index)

	return index
}

type Index struct {
	Name        string
	Columns     []string
	Cardinality []uint64
	DataColumns []string
}

func NewIndex(name string) *Index {
	return &Index{name, make([]string, 0, 8), make([]uint64, 0, 8), nil}
}

func (idx *Index) AddColumn(name string, cardinality uint64) {
	idx.Columns = append(idx.Columns, name)
	if cardinality == 0 {
		cardinality = uint64(len(idx.Cardinality) + 1)
	}
	idx.Cardinality = append(idx.Cardinality, cardinality)
}

func (idx *Index) FindColumn(name string) int {
	for i, colName := range idx.Columns {
		if name == colName {
			return i
		}
	}

	return -1
}

func (idx *Index) FindDataColumn(name string) int {
	for i, colName := range idx.DataColumns {
		if name == colName {
			return i
		}
	}

	return -1
}
<|endoftext|>


<commit_before>package main

import (
	"fmt"
	"github.com/udhos/gowut/gwu"
	"log"
	//"math/rand"
	"os"
	//"strconv"
)

/*
type SessHandler struct{}

func (h SessHandler) Created(s gwu.Session) {
	logger.Println("SESSION created:", s.Id())
	//buildLoginWin(s)
}

func (h SessHandler) Removed(s gwu.Session) {
	logger.Println("SESSION removed:", s.Id())
}
*/

const appName = "jazigo"

var logger = log.New(os.Stdout, "", log.LstdFlags)

func main() {

	appAddr := "0.0.0.0:8080"
	serverName := fmt.Sprintf("%s application", appName)

	// Create GUI server
	server := gwu.NewServer(appName, appAddr)
	//folder := "./tls/"
	//server := gwu.NewServerTLS(appName, appAddr, folder+"cert.pem", folder+"key.pem")
	server.SetText(serverName)

	/*
		server.AddSessCreatorName("login", fmt.Sprintf("%s login window", appName))
		server.AddSHandler(SessHandler{})
	*/

	buildHomeWin(server)
	buildLoginWin(server)

	server.SetLogger(logger)

	// Start GUI server
	if err := server.Start(); err != nil {
		logger.Println("jazigo main: Cound not start GUI server:", err)
		return
	}
}

func buildHomeWin(s gwu.Session) {
	// Add home window
	win := gwu.NewWindow("home", fmt.Sprintf("%s home window", appName))

	l := gwu.NewLabel(fmt.Sprintf("%s home", appName))
	l.Style().SetFontWeight(gwu.FontWeightBold).SetFontSize("130%")
	win.Add(l)
	win.Add(gwu.NewLabel("Click on the button to login:"))
	b := gwu.NewButton("Login")
	b.AddEHandlerFunc(func(e gwu.Event) {
		e.ReloadWin("login")
	}, gwu.ETypeClick)
	win.Add(b)

	s.AddWin(win)
}

func buildLoginWin(s gwu.Session) {
	windowName := fmt.Sprintf("%s login window", appName)

	win := gwu.NewWindow("login", windowName)
	win.Style().SetFullSize()
	win.SetAlign(gwu.HACenter, gwu.VAMiddle)

	p := gwu.NewPanel()
	p.SetHAlign(gwu.HACenter)
	p.SetCellPadding(2)

	l := gwu.NewLabel(windowName)
	l.Style().SetFontWeight(gwu.FontWeightBold).SetFontSize("150%")
	p.Add(l)
	l = gwu.NewLabel("Login")
	l.Style().SetFontWeight(gwu.FontWeightBold).SetFontSize("130%")
	p.Add(l)
	p.CellFmt(l).Style().SetBorder2(1, gwu.BrdStyleDashed, gwu.ClrNavy)
	l = gwu.NewLabel("user/pass: admin/a")
	l.Style().SetFontSize("80%").SetFontStyle(gwu.FontStyleItalic)
	p.Add(l)

	errL := gwu.NewLabel("")
	errL.Style().SetColor(gwu.ClrRed)
	p.Add(errL)

	table := gwu.NewTable()
	table.SetCellPadding(2)
	table.EnsureSize(2, 2)
	table.Add(gwu.NewLabel("Username:"), 0, 0)
	tb := gwu.NewTextBox("")
	tb.Style().SetWidthPx(160)
	table.Add(tb, 0, 1)
	table.Add(gwu.NewLabel("Password:"), 1, 0)
	pb := gwu.NewPasswBox("")
	pb.Style().SetWidthPx(160)
	table.Add(pb, 1, 1)
	p.Add(table)
	b := gwu.NewButton("OK")
	b.AddEHandlerFunc(func(e gwu.Event) {
		if tb.Text() == "admin" && pb.Text() == "a" {
			//e.Session().RemoveWin(win) // Login win is removed, password will not be retrievable from the browser
			//buildPrivateWins(e.Session())
			// FIXME: Should clear username/password fields?
			buildPrivateWins(e.NewSession())
			e.ReloadWin("main")
		} else {
			e.SetFocusedComp(tb)
			errL.SetText("Invalid user name or password!")
			e.MarkDirty(errL)
		}
	}, gwu.ETypeClick)
	p.Add(b)
	l = gwu.NewLabel("")
	p.Add(l)
	p.CellFmt(l).Style().SetHeightPx(200)

	win.Add(p)
	win.SetFocusedCompId(tb.Id())

	s.AddWin(win)
}

func buildPrivateWins(s gwu.Session) {
	// Create and build a window
	winName := fmt.Sprintf("%s main window", appName)
	win := gwu.NewWindow("main", winName)

	win.Style().SetFullWidth()
	win.SetCellPadding(2)

	title := gwu.NewLabel(winName)
	win.Add(title)

	s.AddWin(win)
}
<commit_msg>Save username into session.<commit_after>package main

import (
	"fmt"
	"github.com/udhos/gowut/gwu"
	"log"
	//"math/rand"
	"os"
	//"strconv"
)

const appName = "jazigo"

var logger = log.New(os.Stdout, "", log.LstdFlags)

func main() {

	appAddr := "0.0.0.0:8080"
	serverName := fmt.Sprintf("%s application", appName)

	// Create GUI server
	server := gwu.NewServer(appName, appAddr)
	//folder := "./tls/"
	//server := gwu.NewServerTLS(appName, appAddr, folder+"cert.pem", folder+"key.pem")
	server.SetText(serverName)

	buildHomeWin(server)
	buildLoginWin(server)

	server.SetLogger(logger)

	// Start GUI server
	if err := server.Start(); err != nil {
		logger.Println("jazigo main: Cound not start GUI server:", err)
		return
	}
}

func buildHomeWin(s gwu.Session) {
	// Add home window
	win := gwu.NewWindow("home", fmt.Sprintf("%s home window", appName))

	l := gwu.NewLabel(fmt.Sprintf("%s home", appName))
	l.Style().SetFontWeight(gwu.FontWeightBold).SetFontSize("130%")
	win.Add(l)
	win.Add(gwu.NewLabel("Click on the button to login:"))
	b := gwu.NewButton("Login")
	b.AddEHandlerFunc(func(e gwu.Event) {
		e.ReloadWin("login")
	}, gwu.ETypeClick)
	win.Add(b)

	s.AddWin(win)
}

func buildLoginWin(s gwu.Session) {
	windowName := fmt.Sprintf("%s login window", appName)

	win := gwu.NewWindow("login", windowName)
	win.Style().SetFullSize()
	win.SetAlign(gwu.HACenter, gwu.VAMiddle)

	p := gwu.NewPanel()
	p.SetHAlign(gwu.HACenter)
	p.SetCellPadding(2)

	l := gwu.NewLabel(windowName)
	l.Style().SetFontWeight(gwu.FontWeightBold).SetFontSize("150%")
	p.Add(l)
	l = gwu.NewLabel("Login")
	l.Style().SetFontWeight(gwu.FontWeightBold).SetFontSize("130%")
	p.Add(l)
	p.CellFmt(l).Style().SetBorder2(1, gwu.BrdStyleDashed, gwu.ClrNavy)
	l = gwu.NewLabel("user/pass: admin/a")
	l.Style().SetFontSize("80%").SetFontStyle(gwu.FontStyleItalic)
	p.Add(l)

	errL := gwu.NewLabel("")
	errL.Style().SetColor(gwu.ClrRed)
	p.Add(errL)

	table := gwu.NewTable()
	table.SetCellPadding(2)
	table.EnsureSize(2, 2)
	table.Add(gwu.NewLabel("Username:"), 0, 0)
	tb := gwu.NewTextBox("")
	tb.Style().SetWidthPx(160)
	table.Add(tb, 0, 1)
	table.Add(gwu.NewLabel("Password:"), 1, 0)
	pb := gwu.NewPasswBox("")
	pb.Style().SetWidthPx(160)
	table.Add(pb, 1, 1)
	p.Add(table)
	b := gwu.NewButton("OK")
	b.AddEHandlerFunc(func(e gwu.Event) {
		user := tb.Text()
		if loginAuth(user, pb.Text()) {
			//e.Session().RemoveWin(win) // Login win is removed, password will not be retrievable from the browser
			//buildPrivateWins(e.Session())
			// FIXME: Should clear username/password fields?
			newSession := e.NewSession()
			newSession.SetAttr("username", user)
			buildPrivateWins(newSession)
			e.ReloadWin("main")
		} else {
			e.SetFocusedComp(tb)
			errL.SetText("Invalid user name or password!")
			e.MarkDirty(errL)
		}
	}, gwu.ETypeClick)
	p.Add(b)
	l = gwu.NewLabel("")
	p.Add(l)
	p.CellFmt(l).Style().SetHeightPx(200)

	win.Add(p)
	win.SetFocusedCompId(tb.Id())

	s.AddWin(win)
}

func loginAuth(user, pass string) bool {
	return user == "admin" && pass == "a"
}

func buildPrivateWins(s gwu.Session) {
	// Create and build a window

	user := s.Attr("username").(string)
	addr := "A.A.A.A"

	winName := fmt.Sprintf("%s main window - user=%s - address=%s", appName, user, addr)
	win := gwu.NewWindow("main", winName)

	win.Style().SetFullWidth()
	win.SetCellPadding(2)

	title := gwu.NewLabel(winName)
	win.Add(title)

	s.AddWin(win)
}
<|endoftext|>


<commit_before><commit_msg>	modified:   mainHandler.go<commit_after><|endoftext|>


<commit_before>package autocomplete

import (
	"context"
	"fmt"
	"io/ioutil"
	"os"
	"path"
	"path/filepath"
	"reflect"
	"runtime"
	"strconv"
	"strings"

	"github.com/scaleway/scaleway-cli/internal/args"
	"github.com/scaleway/scaleway-cli/internal/core"
	"github.com/scaleway/scaleway-cli/internal/interactive"
	"github.com/scaleway/scaleway-sdk-go/logger"
)

func GetCommands() *core.Commands {
	return core.NewCommands(
		autocompleteInstallCommand(),
		autocompleteCompleteBashCommand(),
		autocompleteCompleteFishCommand(),
		autocompleteCompleteZshCommand(),
		autocompleteScriptCommand(),
	)
}

type autocompleteScript struct {
	CompleteScript         string
	CompleteFunc           string
	ShellConfigurationFile map[string]string
}

// autocompleteScripts regroups the autocomplete scripts for the different shells
// The key is the path of the shell.
var autocompleteScripts = map[string]autocompleteScript{
	"bash": {
		// If `scw` is the first word on the command line,
		// after hitting [tab] arguments are sent to `scw autocomplete complete bash`:
		//  - COMP_LINE: the complete command line
		//  - cword:     the index of the word being completed (source COMP_CWORD)
		//  - words:     the words composing the command line (source COMP_WORDS)
		//
		// Note that `=` signs are excluding from $COMP_WORDBREAKS. As a result, they are NOT be
		// considered as breaking words and arguments like `image=` will not be split.
		//
		// Then `scw autocomplete complete bash` process the line, and tries to returns suggestions.
		// These scw suggestions are put into `COMPREPLY` which is used by Bash to provides the shell suggestions.
		CompleteFunc: `
			_scw() {
				_get_comp_words_by_ref -n = cword words

				output=$(scw autocomplete complete bash -- "$COMP_LINE" "$cword" "${words[@]}")
				COMPREPLY=($output)
				# apply compopt option and ignore failure for older bash versions
				[[ $COMPREPLY == *= ]] && compopt -o nospace 2> /dev/null || true
				return
			}
			complete -F _scw scw
		`,
		CompleteScript: `eval "$(scw autocomplete script shell=bash)"`,
		ShellConfigurationFile: map[string]string{
			"darwin": path.Join(os.Getenv("HOME"), ".bash_profile"),
		},
	},
	"fish": {
		// (commandline)                             complete command line
		// (commandline --cursor)                    position of the cursor, as number of chars in the command line
		// (commandline --current-token)             word to complete
		// (commandline --tokenize --cut-at-cursor)  tokenized selection up until the current cursor position
		//                                           formatted as one string-type token per line
		//
		// If files are shown although --no-files is set,
		// it might be because you are using an alias for scw, such as :
		// 		alias scw='go run "$HOME"/scaleway-cli/cmd/scw/main.go'
		// You might want to run 'complete --erase --command go' during development.
		//
		// TODO: send rightWords
		CompleteFunc: `
			complete --erase --command scw;
			complete --command scw --no-files;
			complete --command scw --arguments '(scw autocomplete complete fish -- (commandline) (commandline --cursor) (commandline --current-token) (commandline --tokenize --cut-at-cursor))';
		`,
		CompleteScript: `eval (scw autocomplete script shell=fish)`,
		ShellConfigurationFile: map[string]string{
			"darwin": path.Join(os.Getenv("HOME"), ".config/fish/config.fish"),
		},
	},
	"zsh": {
		// If you are using an alias for scw, such as :
		// 		alias scw='go run "$HOME"/scaleway-cli/cmd/scw/main.go'
		// you might want to run 'compdef _scw go' during development.
		CompleteFunc: `
			_scw () {
				# splits $BUFFER, i.e. the complete command line,
				# into shell words using shell parsing rules by Expansion Flag (z) and puts it into an array
				words=("${(z)BUFFER}")

				# If the last char of the line is a space, a last empty word is not added to words.
				# We need to add it manually.
				lastChar="${BUFFER: -1}"
				if [[ $lastChar = *[!\ ]* ]]; then # if $lastChar contains something else than spaces
					: # do nothing
				else
					# words+=('') does not work
					# couldn't find a way to add an empty string to an array
					# we replace 'EMPTY_WORD' by '' later in go code
					words+=('EMPTY_WORD')
				fi
				output=($(scw autocomplete complete zsh -- $CURSOR $words))
				opts=('-S' ' ')
				if [[ $output == *= ]]; then
					opts=('-S' '')
				fi
				compadd "${opts[@]}" -- "${output[@]}"
			}
			compdef _scw scw
		`,
		CompleteScript: `eval "$(scw autocomplete script shell=zsh)"`,
		ShellConfigurationFile: map[string]string{
			"darwin": path.Join(os.Getenv("HOME"), ".zshrc"),
		},
	},
}

type InstallArgs struct {
	Shell string
}

func autocompleteInstallCommand() *core.Command {
	return &core.Command{
		Short:     `Install autocompletion script`,
		Long:      `Install autocompletion script for a given shell and OS.`,
		Namespace: "autocomplete",
		Resource:  "install",
		NoClient:  true,
		ArgSpecs: core.ArgSpecs{
			{
				Name: "shell",
			},
		},
		ArgsType: reflect.TypeOf(InstallArgs{}),
		Run:      InstallCommandRun,
	}
}

func InstallCommandRun(ctx context.Context, argsI interface{}) (i interface{}, e error) {
	// Warning
	_, _ = interactive.Println("To enable autocomplete, scw needs to update your shell configuration")

	// If `shell=` is empty, ask for a value for `shell=`.
	shellArg := argsI.(*InstallArgs).Shell
	logger.Debugf("shellArg: %v", shellArg)
	if shellArg == "" {
		defaultShellName := filepath.Base(os.Getenv("SHELL"))

		promptedShell, err := interactive.PromptStringWithConfig(&interactive.PromptStringConfig{
			Prompt:          "What type of shell are you using",
			DefaultValue:    defaultShellName,
			DefaultValueDoc: defaultShellName,
		})
		if err != nil {
			return nil, err
		}
		shellArg = promptedShell
	}

	shellName := filepath.Base(shellArg)

	script, exists := autocompleteScripts[shellName]
	if !exists {
		return nil, unsupportedShellError(shellName)
	}

	// Find destination file depending on the OS.
	shellConfigurationFilePath, exists := script.ShellConfigurationFile[runtime.GOOS]
	if !exists {
		return nil, unsupportedOsError(runtime.GOOS)
	}

	// If the file doesn't exist, create it
	f, err := os.OpenFile(shellConfigurationFilePath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
	if f != nil {
		defer f.Close()
	}
	if err != nil {
		return nil, err
	}

	// Early exit if eval line is already present in the shell configuration.
	shellConfigurationFileContent, err := ioutil.ReadAll(f)
	if err != nil {
		return nil, err
	}
	if strings.Contains(string(shellConfigurationFileContent), script.CompleteScript) {
		_, _ = interactive.Println("Autocomplete looks already installed. If it does not work properly, try to open a new shell.")
		return "", nil
	}

	// Warning
	_, _ = interactive.Println("To enable autocompletion we need to append to " + shellConfigurationFilePath + " the following line:\n\t" + script.CompleteScript)

	// Early exit if user disagrees
	continueInstallation, err := interactive.PromptBoolWithConfig(&interactive.PromptBoolConfig{
		Prompt:       fmt.Sprintf("Do you want to proceed with theses changes ?"),
		DefaultValue: true,
	})
	if err != nil {
		return nil, err
	}
	if !continueInstallation {
		return nil, installationCancelledError(shellName, script.CompleteScript)
	}

	// Append to file
	_, err = f.Write([]byte(script.CompleteScript + "\n"))
	if err != nil {
		return nil, err
	}

	// Ack
	return &core.SuccessResult{
		Message: fmt.Sprintf("Autocomplete function for %v installed successfully.\nUpdated %v.", shellName, shellConfigurationFilePath),
	}, nil
}

func autocompleteCompleteBashCommand() *core.Command {
	return &core.Command{
		Short:     `Autocomplete for Bash`,
		Long:      `Autocomplete for Bash.`,
		Namespace: "autocomplete",
		Resource:  "complete",
		Verb:      "bash",
		// TODO: Switch NoClient to true when cache will be implemented.
		NoClient: false,
		Hidden:   true,
		ArgsType: reflect.TypeOf(args.RawArgs{}),
		Run: func(ctx context.Context, argsI interface{}) (i interface{}, e error) {
			rawArgs := *argsI.(*args.RawArgs)
			wordIndex, err := strconv.Atoi(rawArgs[1])
			if err != nil {
				return nil, err
			}
			words := rawArgs[2:]
			leftWords := words[:wordIndex]
			wordToComplete := words[wordIndex]
			rightWords := words[wordIndex+1:]

			// If the wordToComplete is an argument label (cf. `arg=`), remove
			// this prefix for all suggestions.
			res := core.AutoComplete(ctx, leftWords, wordToComplete, rightWords)
			if strings.Contains(wordToComplete, "=") {
				prefix := strings.SplitAfterN(wordToComplete, "=", 2)[0]
				for k, p := range res.Suggestions {
					res.Suggestions[k] = strings.TrimPrefix(p, prefix)
				}
			}

			return strings.Join(res.Suggestions, " "), nil
		},
	}
}

func autocompleteCompleteFishCommand() *core.Command {
	return &core.Command{
		Short:     `Autocomplete for Fish`,
		Long:      `Autocomplete for Fish.`,
		Namespace: "autocomplete",
		Resource:  "complete",
		Verb:      "fish",
		// TODO: Switch NoClient to true when cache will be implemented.
		NoClient: false,
		Hidden:   true,
		ArgsType: reflect.TypeOf(args.RawArgs{}),
		Run: func(ctx context.Context, argsI interface{}) (i interface{}, e error) {
			rawArgs := *argsI.(*args.RawArgs)
			leftWords := rawArgs[3:]
			wordToComplete := rawArgs[2]

			// TODO: compute rightWords once used by core.AutoComplete()
			// line := rawArgs[0]
			// charIndex, _ := strconv.Atoi(rawArgs[1])
			rightWords := []string(nil)

			res := core.AutoComplete(ctx, leftWords, wordToComplete, rightWords)

			// TODO: decide if we want to add descriptions
			// see https://stackoverflow.com/a/20879411
			// "followed optionally by a tab and a short description."
			return strings.Join(res.Suggestions, "\n"), nil
		},
	}
}

func autocompleteCompleteZshCommand() *core.Command {
	return &core.Command{
		Short:     `Autocomplete for Zsh`,
		Long:      `Autocomplete for Zsh.`,
		Namespace: "autocomplete",
		Resource:  "complete",
		Verb:      "zsh",
		// TODO: Switch NoClient to true when cache will be implemented.
		NoClient: false,
		Hidden:   true,
		ArgsType: reflect.TypeOf(args.RawArgs{}),
		Run: func(ctx context.Context, argsI interface{}) (i interface{}, e error) {
			rawArgs := *argsI.(*args.RawArgs)

			words := rawArgs[1:]
			charIndex, err := strconv.Atoi(rawArgs[0])
			if err != nil {
				return nil, err
			}
			wordIndex := core.WordIndex(charIndex, words)
			leftWords := words[:wordIndex]
			wordToComplete := words[wordIndex]

			// In zsh, couldn't find a way to add an empty string to an array.
			// We added "EMPTY_WORD" instead.
			// "EMPTY_WORD" is replaced by "".
			// see the zsh script, line 106:
			//     words+=('EMPTY_WORD')
			if wordToComplete == "EMPTY_WORD" {
				wordToComplete = ""
			}

			// TODO: compute rightWords once used by core.AutoComplete()
			rightWords := []string(nil)

			res := core.AutoComplete(ctx, leftWords, wordToComplete, rightWords)
			return strings.Join(res.Suggestions, " "), nil
		},
	}
}

type autocompleteShowArgs struct {
	Shell string
}

func autocompleteScriptCommand() *core.Command {
	return &core.Command{
		Short:     `Show autocomplete script for current shell`,
		Long:      `Show autocomplete script for current shell.`,
		Namespace: "autocomplete",
		Resource:  "script",
		NoClient:  true,
		ArgSpecs: core.ArgSpecs{
			{
				Name:    "shell",
				Default: core.DefaultValueSetter(os.Getenv("SHELL")),
			},
		},
		ArgsType: reflect.TypeOf(autocompleteShowArgs{}),
		Run: func(ctx context.Context, argsI interface{}) (i interface{}, e error) {
			shell := filepath.Base(argsI.(*autocompleteShowArgs).Shell)
			script, exists := autocompleteScripts[shell]
			if !exists {
				return nil, unsupportedShellError(shell)
			}
			return trimText(script.CompleteFunc), nil
		},
	}
}

func trimText(str string) string {
	foundFirstNonEmptyLine := false
	strToRemove := ""
	lines := strings.Split(str, "\n")
	for i, line := range lines {
		if !foundFirstNonEmptyLine {
			if len(line) > 0 {
				for _, c := range line {
					if c == ' ' || c == '\t' {
						strToRemove += string(c)
						continue
					}
					break
				}
				foundFirstNonEmptyLine = true
			}
		}
		for _, c := range strToRemove {
			lines[i] = strings.Replace(lines[i], string(c), "", 1)
		}
	}
	lines = removeStartingAndEndingEmptyLines(lines)
	return strings.Join(lines, "\n")
}

func removeStartingAndEndingEmptyLines(lines []string) []string {
	lines = removeStartingEmptyLines(lines)
	lines = reverseLines(lines)
	lines = removeStartingEmptyLines(lines)
	lines = reverseLines(lines)
	return lines
}

func removeStartingEmptyLines(lines []string) []string {
	doAdd := false
	lines2 := []string(nil)
	for _, line := range lines {
		if len(line) > 0 {
			doAdd = true
		}
		if doAdd {
			lines2 = append(lines2, line)
		}
	}
	return lines2
}

func reverseLines(lines []string) []string {
	for i, j := 0, len(lines)-1; i < j; i, j = i+1, j-1 {
		lines[i], lines[j] = lines[j], lines[i]
	}
	return lines
}
<commit_msg>fix: make autocomplete on zsh usable (#676)<commit_after>package autocomplete

import (
	"context"
	"fmt"
	"io/ioutil"
	"os"
	"path"
	"path/filepath"
	"reflect"
	"runtime"
	"strconv"
	"strings"

	"github.com/scaleway/scaleway-cli/internal/args"
	"github.com/scaleway/scaleway-cli/internal/core"
	"github.com/scaleway/scaleway-cli/internal/interactive"
	"github.com/scaleway/scaleway-sdk-go/logger"
)

func GetCommands() *core.Commands {
	return core.NewCommands(
		autocompleteInstallCommand(),
		autocompleteCompleteBashCommand(),
		autocompleteCompleteFishCommand(),
		autocompleteCompleteZshCommand(),
		autocompleteScriptCommand(),
	)
}

type autocompleteScript struct {
	CompleteScript         string
	CompleteFunc           string
	ShellConfigurationFile map[string]string
}

// autocompleteScripts regroups the autocomplete scripts for the different shells
// The key is the path of the shell.
var autocompleteScripts = map[string]autocompleteScript{
	"bash": {
		// If `scw` is the first word on the command line,
		// after hitting [tab] arguments are sent to `scw autocomplete complete bash`:
		//  - COMP_LINE: the complete command line
		//  - cword:     the index of the word being completed (source COMP_CWORD)
		//  - words:     the words composing the command line (source COMP_WORDS)
		//
		// Note that `=` signs are excluding from $COMP_WORDBREAKS. As a result, they are NOT be
		// considered as breaking words and arguments like `image=` will not be split.
		//
		// Then `scw autocomplete complete bash` process the line, and tries to returns suggestions.
		// These scw suggestions are put into `COMPREPLY` which is used by Bash to provides the shell suggestions.
		CompleteFunc: `
			_scw() {
				_get_comp_words_by_ref -n = cword words

				output=$(scw autocomplete complete bash -- "$COMP_LINE" "$cword" "${words[@]}")
				COMPREPLY=($output)
				# apply compopt option and ignore failure for older bash versions
				[[ $COMPREPLY == *= ]] && compopt -o nospace 2> /dev/null || true
				return
			}
			complete -F _scw scw
		`,
		CompleteScript: `eval "$(scw autocomplete script shell=bash)"`,
		ShellConfigurationFile: map[string]string{
			"darwin": path.Join(os.Getenv("HOME"), ".bash_profile"),
		},
	},
	"fish": {
		// (commandline)                             complete command line
		// (commandline --cursor)                    position of the cursor, as number of chars in the command line
		// (commandline --current-token)             word to complete
		// (commandline --tokenize --cut-at-cursor)  tokenized selection up until the current cursor position
		//                                           formatted as one string-type token per line
		//
		// If files are shown although --no-files is set,
		// it might be because you are using an alias for scw, such as :
		// 		alias scw='go run "$HOME"/scaleway-cli/cmd/scw/main.go'
		// You might want to run 'complete --erase --command go' during development.
		//
		// TODO: send rightWords
		CompleteFunc: `
			complete --erase --command scw;
			complete --command scw --no-files;
			complete --command scw --arguments '(scw autocomplete complete fish -- (commandline) (commandline --cursor) (commandline --current-token) (commandline --tokenize --cut-at-cursor))';
		`,
		CompleteScript: `eval (scw autocomplete script shell=fish)`,
		ShellConfigurationFile: map[string]string{
			"darwin": path.Join(os.Getenv("HOME"), ".config/fish/config.fish"),
		},
	},
	"zsh": {
		// If you are using an alias for scw, such as :
		// 		alias scw='go run "$HOME"/scaleway-cli/cmd/scw/main.go'
		// you might want to run 'compdef _scw go' during development.
		CompleteFunc: `
			autoload -U compinit && compinit
			_scw () {
				# splits $BUFFER, i.e. the complete command line,
				# into shell words using shell parsing rules by Expansion Flag (z) and puts it into an array
				words=("${(z)BUFFER}")

				# If the last char of the line is a space, a last empty word is not added to words.
				# We need to add it manually.
				lastChar="${BUFFER: -1}"
				if [[ $lastChar = *[!\ ]* ]]; then # if $lastChar contains something else than spaces
					: # do nothing
				else
					# words+=('') does not work
					# couldn't find a way to add an empty string to an array
					# we replace 'EMPTY_WORD' by '' later in go code
					words+=('EMPTY_WORD')
				fi
				output=($(scw autocomplete complete zsh -- $CURSOR $words))
				opts=('-S' ' ')
				if [[ $output == *= ]]; then
					opts=('-S' '')
				fi
				compadd "${opts[@]}" -- "${output[@]}"
			}
			compdef _scw scw
		`,
		CompleteScript: `eval "$(scw autocomplete script shell=zsh)"`,
		ShellConfigurationFile: map[string]string{
			"darwin": path.Join(os.Getenv("HOME"), ".zshrc"),
		},
	},
}

type InstallArgs struct {
	Shell string
}

func autocompleteInstallCommand() *core.Command {
	return &core.Command{
		Short:     `Install autocompletion script`,
		Long:      `Install autocompletion script for a given shell and OS.`,
		Namespace: "autocomplete",
		Resource:  "install",
		NoClient:  true,
		ArgSpecs: core.ArgSpecs{
			{
				Name: "shell",
			},
		},
		ArgsType: reflect.TypeOf(InstallArgs{}),
		Run:      InstallCommandRun,
	}
}

func InstallCommandRun(ctx context.Context, argsI interface{}) (i interface{}, e error) {
	// Warning
	_, _ = interactive.Println("To enable autocomplete, scw needs to update your shell configuration")

	// If `shell=` is empty, ask for a value for `shell=`.
	shellArg := argsI.(*InstallArgs).Shell
	logger.Debugf("shellArg: %v", shellArg)
	if shellArg == "" {
		defaultShellName := filepath.Base(os.Getenv("SHELL"))

		promptedShell, err := interactive.PromptStringWithConfig(&interactive.PromptStringConfig{
			Prompt:          "What type of shell are you using",
			DefaultValue:    defaultShellName,
			DefaultValueDoc: defaultShellName,
		})
		if err != nil {
			return nil, err
		}
		shellArg = promptedShell
	}

	shellName := filepath.Base(shellArg)

	script, exists := autocompleteScripts[shellName]
	if !exists {
		return nil, unsupportedShellError(shellName)
	}

	// Find destination file depending on the OS.
	shellConfigurationFilePath, exists := script.ShellConfigurationFile[runtime.GOOS]
	if !exists {
		return nil, unsupportedOsError(runtime.GOOS)
	}

	// If the file doesn't exist, create it
	f, err := os.OpenFile(shellConfigurationFilePath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
	if f != nil {
		defer f.Close()
	}
	if err != nil {
		return nil, err
	}

	// Early exit if eval line is already present in the shell configuration.
	shellConfigurationFileContent, err := ioutil.ReadAll(f)
	if err != nil {
		return nil, err
	}
	if strings.Contains(string(shellConfigurationFileContent), script.CompleteScript) {
		_, _ = interactive.Println("Autocomplete looks already installed. If it does not work properly, try to open a new shell.")
		return "", nil
	}

	// Warning
	_, _ = interactive.Println("To enable autocompletion we need to append to " + shellConfigurationFilePath + " the following line:\n\t" + script.CompleteScript)

	// Early exit if user disagrees
	continueInstallation, err := interactive.PromptBoolWithConfig(&interactive.PromptBoolConfig{
		Prompt:       fmt.Sprintf("Do you want to proceed with theses changes ?"),
		DefaultValue: true,
	})
	if err != nil {
		return nil, err
	}
	if !continueInstallation {
		return nil, installationCancelledError(shellName, script.CompleteScript)
	}

	// Append to file
	_, err = f.Write([]byte(script.CompleteScript + "\n"))
	if err != nil {
		return nil, err
	}

	// Ack
	return &core.SuccessResult{
		Message: fmt.Sprintf("Autocomplete function for %v installed successfully.\nUpdated %v.", shellName, shellConfigurationFilePath),
	}, nil
}

func autocompleteCompleteBashCommand() *core.Command {
	return &core.Command{
		Short:     `Autocomplete for Bash`,
		Long:      `Autocomplete for Bash.`,
		Namespace: "autocomplete",
		Resource:  "complete",
		Verb:      "bash",
		// TODO: Switch NoClient to true when cache will be implemented.
		NoClient: false,
		Hidden:   true,
		ArgsType: reflect.TypeOf(args.RawArgs{}),
		Run: func(ctx context.Context, argsI interface{}) (i interface{}, e error) {
			rawArgs := *argsI.(*args.RawArgs)
			wordIndex, err := strconv.Atoi(rawArgs[1])
			if err != nil {
				return nil, err
			}
			words := rawArgs[2:]
			leftWords := words[:wordIndex]
			wordToComplete := words[wordIndex]
			rightWords := words[wordIndex+1:]

			// If the wordToComplete is an argument label (cf. `arg=`), remove
			// this prefix for all suggestions.
			res := core.AutoComplete(ctx, leftWords, wordToComplete, rightWords)
			if strings.Contains(wordToComplete, "=") {
				prefix := strings.SplitAfterN(wordToComplete, "=", 2)[0]
				for k, p := range res.Suggestions {
					res.Suggestions[k] = strings.TrimPrefix(p, prefix)
				}
			}

			return strings.Join(res.Suggestions, " "), nil
		},
	}
}

func autocompleteCompleteFishCommand() *core.Command {
	return &core.Command{
		Short:     `Autocomplete for Fish`,
		Long:      `Autocomplete for Fish.`,
		Namespace: "autocomplete",
		Resource:  "complete",
		Verb:      "fish",
		// TODO: Switch NoClient to true when cache will be implemented.
		NoClient: false,
		Hidden:   true,
		ArgsType: reflect.TypeOf(args.RawArgs{}),
		Run: func(ctx context.Context, argsI interface{}) (i interface{}, e error) {
			rawArgs := *argsI.(*args.RawArgs)
			leftWords := rawArgs[3:]
			wordToComplete := rawArgs[2]

			// TODO: compute rightWords once used by core.AutoComplete()
			// line := rawArgs[0]
			// charIndex, _ := strconv.Atoi(rawArgs[1])
			rightWords := []string(nil)

			res := core.AutoComplete(ctx, leftWords, wordToComplete, rightWords)

			// TODO: decide if we want to add descriptions
			// see https://stackoverflow.com/a/20879411
			// "followed optionally by a tab and a short description."
			return strings.Join(res.Suggestions, "\n"), nil
		},
	}
}

func autocompleteCompleteZshCommand() *core.Command {
	return &core.Command{
		Short:     `Autocomplete for Zsh`,
		Long:      `Autocomplete for Zsh.`,
		Namespace: "autocomplete",
		Resource:  "complete",
		Verb:      "zsh",
		// TODO: Switch NoClient to true when cache will be implemented.
		NoClient: false,
		Hidden:   true,
		ArgsType: reflect.TypeOf(args.RawArgs{}),
		Run: func(ctx context.Context, argsI interface{}) (i interface{}, e error) {
			rawArgs := *argsI.(*args.RawArgs)

			words := rawArgs[1:]
			charIndex, err := strconv.Atoi(rawArgs[0])
			if err != nil {
				return nil, err
			}
			wordIndex := core.WordIndex(charIndex, words)
			leftWords := words[:wordIndex]
			wordToComplete := words[wordIndex]

			// In zsh, couldn't find a way to add an empty string to an array.
			// We added "EMPTY_WORD" instead.
			// "EMPTY_WORD" is replaced by "".
			// see the zsh script, line 106:
			//     words+=('EMPTY_WORD')
			if wordToComplete == "EMPTY_WORD" {
				wordToComplete = ""
			}

			// TODO: compute rightWords once used by core.AutoComplete()
			rightWords := []string(nil)

			res := core.AutoComplete(ctx, leftWords, wordToComplete, rightWords)
			return strings.Join(res.Suggestions, " "), nil
		},
	}
}

type autocompleteShowArgs struct {
	Shell string
}

func autocompleteScriptCommand() *core.Command {
	return &core.Command{
		Short:     `Show autocomplete script for current shell`,
		Long:      `Show autocomplete script for current shell.`,
		Namespace: "autocomplete",
		Resource:  "script",
		NoClient:  true,
		ArgSpecs: core.ArgSpecs{
			{
				Name:    "shell",
				Default: core.DefaultValueSetter(os.Getenv("SHELL")),
			},
		},
		ArgsType: reflect.TypeOf(autocompleteShowArgs{}),
		Run: func(ctx context.Context, argsI interface{}) (i interface{}, e error) {
			shell := filepath.Base(argsI.(*autocompleteShowArgs).Shell)
			script, exists := autocompleteScripts[shell]
			if !exists {
				return nil, unsupportedShellError(shell)
			}
			return trimText(script.CompleteFunc), nil
		},
	}
}

func trimText(str string) string {
	foundFirstNonEmptyLine := false
	strToRemove := ""
	lines := strings.Split(str, "\n")
	for i, line := range lines {
		if !foundFirstNonEmptyLine {
			if len(line) > 0 {
				for _, c := range line {
					if c == ' ' || c == '\t' {
						strToRemove += string(c)
						continue
					}
					break
				}
				foundFirstNonEmptyLine = true
			}
		}
		for _, c := range strToRemove {
			lines[i] = strings.Replace(lines[i], string(c), "", 1)
		}
	}
	lines = removeStartingAndEndingEmptyLines(lines)
	return strings.Join(lines, "\n")
}

func removeStartingAndEndingEmptyLines(lines []string) []string {
	lines = removeStartingEmptyLines(lines)
	lines = reverseLines(lines)
	lines = removeStartingEmptyLines(lines)
	lines = reverseLines(lines)
	return lines
}

func removeStartingEmptyLines(lines []string) []string {
	doAdd := false
	lines2 := []string(nil)
	for _, line := range lines {
		if len(line) > 0 {
			doAdd = true
		}
		if doAdd {
			lines2 = append(lines2, line)
		}
	}
	return lines2
}

func reverseLines(lines []string) []string {
	for i, j := 0, len(lines)-1; i < j; i, j = i+1, j-1 {
		lines[i], lines[j] = lines[j], lines[i]
	}
	return lines
}
<|endoftext|>


<commit_before>package server_test

import (
	"net"
	"sync"
	"testing"
	"time"

	"github.com/peer-calls/peer-calls/server"
	"github.com/peer-calls/peer-calls/server/test"
	"github.com/pion/rtp"
	"github.com/pion/rtp/codecs"
	"github.com/pion/webrtc/v2/pkg/media"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"go.uber.org/goleak"
)

func listenUDP(laddr *net.UDPAddr) *net.UDPConn {
	conn, err := net.ListenUDP("udp", laddr)
	if err != nil {
		panic(err)
	}
	return conn
}

func TestTransportManager_RTP(t *testing.T) {
	goleak.VerifyNone(t)
	defer goleak.VerifyNone(t)

	loggerFactory := test.NewLoggerFactory()

	udpConn1 := listenUDP(&net.UDPAddr{
		IP:   net.IP{127, 0, 0, 1},
		Port: 0,
	})
	defer udpConn1.Close()

	udpConn2 := listenUDP(&net.UDPAddr{
		IP:   net.IP{127, 0, 0, 1},
		Port: 0,
	})
	defer udpConn2.Close()

	var f1, f2 *server.ServerTransportFactory

	tm1 := server.NewTransportManager(server.TransportManagerParams{
		Conn:          udpConn1,
		LoggerFactory: loggerFactory,
	})
	defer tm1.Close()

	tm2 := server.NewTransportManager(server.TransportManagerParams{
		Conn:          udpConn2,
		LoggerFactory: loggerFactory,
	})
	defer tm2.Close()

	sample := media.Sample{Data: []byte{0x00, 0x01, 0x02}, Samples: 1}

	var vp8Packetizer = rtp.NewPacketizer(
		1200,
		96,
		12345678,
		&codecs.VP8Payloader{},
		rtp.NewRandomSequencer(),
		96000,
	)

	rtpPackets := vp8Packetizer.Packetize(sample.Data, sample.Samples)
	require.Equal(t, 1, len(rtpPackets), "expected only a single RTP packet")

	rtpPacketBytes, err := rtpPackets[0].Marshal()
	require.NoError(t, err)

	// prevent race condition between transport.WriteRTP in goroutine 1 and
	// assert.Equal on recv.
	rtpPacketBytesCopy := make([]byte, len(rtpPacketBytes))
	copy(rtpPacketBytesCopy, rtpPacketBytes)

	var wg sync.WaitGroup
	wg.Add(2)

	var transport1, transport2 server.Transport

	go func() {
		defer wg.Done()
		var err error

		f1, err = tm1.AcceptTransportFactory()
		require.NoError(t, err)

		transport, err := f1.AcceptTransport().WaitTimeout(20 * time.Second)
		require.NoError(t, err)
		assert.Equal(t, "test-stream", transport.StreamID)

		for _, rtpPacket := range rtpPackets {
			i, err := transport.WriteRTP(rtpPacket)
			assert.NoError(t, err)
			assert.Equal(t, rtpPacket.MarshalSize(), i, "expected to send RTP bytes")
		}

		transport1 = transport
	}()

	go func() {
		defer wg.Done()
		var err error
		f2, err = tm2.GetTransportFactory(udpConn1.LocalAddr())
		require.NoError(t, err)

		transport, err := f2.NewTransport("test-stream").WaitTimeout(20 * time.Second)
		require.NoError(t, err)

		select {
		case pkt := <-transport.RTPChannel():
			assert.Equal(t, rtpPacketBytesCopy, pkt.Raw)
		case <-time.After(time.Second):
			assert.Fail(t, "Timed out waiting for rtp.Packet")
		}

		transport2 = transport
	}()

	wg.Wait()

	assert.NoError(t, transport1.Close())
	assert.NoError(t, transport2.Close())
}

func TestTransportManager_NewTransport_Cancel(t *testing.T) {
	goleak.VerifyNone(t)
	defer goleak.VerifyNone(t)

	loggerFactory := test.NewLoggerFactory()

	udpConn1 := listenUDP(&net.UDPAddr{
		IP:   net.IP{127, 0, 0, 1},
		Port: 0,
	})
	defer udpConn1.Close()

	tm1 := server.NewTransportManager(server.TransportManagerParams{
		Conn:          udpConn1,
		LoggerFactory: loggerFactory,
	})
	defer tm1.Close()

	var err error
	f2, err := tm1.GetTransportFactory(udpConn1.LocalAddr())
	require.NoError(t, err)

	transportPromise := f2.NewTransport("test-stream")

	var wg sync.WaitGroup
	wg.Add(1)

	go func() {
		defer wg.Done()
		transport, err := transportPromise.WaitTimeout(20 * time.Second)
		require.Equal(t, server.ErrCanceled, err)
		require.Nil(t, transport)
	}()

	transportPromise.Cancel()

	wg.Wait()
}
<commit_msg>Remove assertion for TransportPromise.Cancel()<commit_after>package server_test

import (
	"net"
	"sync"
	"testing"
	"time"

	"github.com/peer-calls/peer-calls/server"
	"github.com/peer-calls/peer-calls/server/test"
	"github.com/pion/rtp"
	"github.com/pion/rtp/codecs"
	"github.com/pion/webrtc/v2/pkg/media"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
	"go.uber.org/goleak"
)

func listenUDP(laddr *net.UDPAddr) *net.UDPConn {
	conn, err := net.ListenUDP("udp", laddr)
	if err != nil {
		panic(err)
	}
	return conn
}

func TestTransportManager_RTP(t *testing.T) {
	goleak.VerifyNone(t)
	defer goleak.VerifyNone(t)

	loggerFactory := test.NewLoggerFactory()

	udpConn1 := listenUDP(&net.UDPAddr{
		IP:   net.IP{127, 0, 0, 1},
		Port: 0,
	})
	defer udpConn1.Close()

	udpConn2 := listenUDP(&net.UDPAddr{
		IP:   net.IP{127, 0, 0, 1},
		Port: 0,
	})
	defer udpConn2.Close()

	var f1, f2 *server.ServerTransportFactory

	tm1 := server.NewTransportManager(server.TransportManagerParams{
		Conn:          udpConn1,
		LoggerFactory: loggerFactory,
	})
	defer tm1.Close()

	tm2 := server.NewTransportManager(server.TransportManagerParams{
		Conn:          udpConn2,
		LoggerFactory: loggerFactory,
	})
	defer tm2.Close()

	sample := media.Sample{Data: []byte{0x00, 0x01, 0x02}, Samples: 1}

	var vp8Packetizer = rtp.NewPacketizer(
		1200,
		96,
		12345678,
		&codecs.VP8Payloader{},
		rtp.NewRandomSequencer(),
		96000,
	)

	rtpPackets := vp8Packetizer.Packetize(sample.Data, sample.Samples)
	require.Equal(t, 1, len(rtpPackets), "expected only a single RTP packet")

	rtpPacketBytes, err := rtpPackets[0].Marshal()
	require.NoError(t, err)

	// prevent race condition between transport.WriteRTP in goroutine 1 and
	// assert.Equal on recv.
	rtpPacketBytesCopy := make([]byte, len(rtpPacketBytes))
	copy(rtpPacketBytesCopy, rtpPacketBytes)

	var wg sync.WaitGroup
	wg.Add(2)

	var transport1, transport2 server.Transport

	go func() {
		defer wg.Done()
		var err error

		f1, err = tm1.AcceptTransportFactory()
		require.NoError(t, err)

		transport, err := f1.AcceptTransport().WaitTimeout(20 * time.Second)
		require.NoError(t, err)
		assert.Equal(t, "test-stream", transport.StreamID)

		for _, rtpPacket := range rtpPackets {
			i, err := transport.WriteRTP(rtpPacket)
			assert.NoError(t, err)
			assert.Equal(t, rtpPacket.MarshalSize(), i, "expected to send RTP bytes")
		}

		transport1 = transport
	}()

	go func() {
		defer wg.Done()
		var err error
		f2, err = tm2.GetTransportFactory(udpConn1.LocalAddr())
		require.NoError(t, err)

		transport, err := f2.NewTransport("test-stream").WaitTimeout(20 * time.Second)
		require.NoError(t, err)

		select {
		case pkt := <-transport.RTPChannel():
			assert.Equal(t, rtpPacketBytesCopy, pkt.Raw)
		case <-time.After(time.Second):
			assert.Fail(t, "Timed out waiting for rtp.Packet")
		}

		transport2 = transport
	}()

	wg.Wait()

	assert.NoError(t, transport1.Close())
	assert.NoError(t, transport2.Close())
}

func TestTransportManager_NewTransport_Cancel(t *testing.T) {
	goleak.VerifyNone(t)
	defer goleak.VerifyNone(t)

	loggerFactory := test.NewLoggerFactory()

	udpConn1 := listenUDP(&net.UDPAddr{
		IP:   net.IP{127, 0, 0, 1},
		Port: 0,
	})
	defer udpConn1.Close()

	tm1 := server.NewTransportManager(server.TransportManagerParams{
		Conn:          udpConn1,
		LoggerFactory: loggerFactory,
	})
	defer tm1.Close()

	var err error
	f2, err := tm1.GetTransportFactory(udpConn1.LocalAddr())
	require.NoError(t, err)

	transportPromise := f2.NewTransport("test-stream")

	var wg sync.WaitGroup
	wg.Add(1)

	go func() {
		defer wg.Done()
		transport, err := transportPromise.WaitTimeout(20 * time.Second)
		_, _ = transport, err
		// Do not assert here because a test might fail if a transport is created
		// before Cancel is called. Rare, but happens.
		// require.Equal(t, server.ErrCanceled, err)
		// require.Nil(t, transport)
	}()

	transportPromise.Cancel()

	wg.Wait()
}
<|endoftext|>


<commit_before>/*
Copyright 2017 The Kubernetes Authors.

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.
*/

package watch

import (
	"context"
	"errors"
	"fmt"
	"io"
	"net/http"
	"time"

	"github.com/davecgh/go-spew/spew"

	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/util/net"
	"k8s.io/apimachinery/pkg/util/wait"
	"k8s.io/apimachinery/pkg/watch"
	"k8s.io/client-go/tools/cache"
	"k8s.io/klog/v2"
)

// resourceVersionGetter is an interface used to get resource version from events.
// We can't reuse an interface from meta otherwise it would be a cyclic dependency and we need just this one method
type resourceVersionGetter interface {
	GetResourceVersion() string
}

// RetryWatcher will make sure that in case the underlying watcher is closed (e.g. due to API timeout or etcd timeout)
// it will get restarted from the last point without the consumer even knowing about it.
// RetryWatcher does that by inspecting events and keeping track of resourceVersion.
// Especially useful when using watch.UntilWithoutRetry where premature termination is causing issues and flakes.
// Please note that this is not resilient to etcd cache not having the resource version anymore - you would need to
// use Informers for that.
type RetryWatcher struct {
	lastResourceVersion string
	watcherClient       cache.Watcher
	resultChan          chan watch.Event
	stopChan            chan struct{}
	doneChan            chan struct{}
	minRestartDelay     time.Duration
}

// NewRetryWatcher creates a new RetryWatcher.
// It will make sure that watches gets restarted in case of recoverable errors.
// The initialResourceVersion will be given to watch method when first called.
func NewRetryWatcher(initialResourceVersion string, watcherClient cache.Watcher) (*RetryWatcher, error) {
	return newRetryWatcher(initialResourceVersion, watcherClient, 1*time.Second)
}

func newRetryWatcher(initialResourceVersion string, watcherClient cache.Watcher, minRestartDelay time.Duration) (*RetryWatcher, error) {
	switch initialResourceVersion {
	case "", "0":
		// TODO: revisit this if we ever get WATCH v2 where it means start "now"
		//       without doing the synthetic list of objects at the beginning (see #74022)
		return nil, fmt.Errorf("initial RV %q is not supported due to issues with underlying WATCH", initialResourceVersion)
	default:
		break
	}

	rw := &RetryWatcher{
		lastResourceVersion: initialResourceVersion,
		watcherClient:       watcherClient,
		stopChan:            make(chan struct{}),
		doneChan:            make(chan struct{}),
		resultChan:          make(chan watch.Event, 0),
		minRestartDelay:     minRestartDelay,
	}

	go rw.receive()
	return rw, nil
}

func (rw *RetryWatcher) send(event watch.Event) bool {
	// Writing to an unbuffered channel is blocking operation
	// and we need to check if stop wasn't requested while doing so.
	select {
	case rw.resultChan <- event:
		return true
	case <-rw.stopChan:
		return false
	}
}

// doReceive returns true when it is done, false otherwise.
// If it is not done the second return value holds the time to wait before calling it again.
func (rw *RetryWatcher) doReceive() (bool, time.Duration) {
	watcher, err := rw.watcherClient.Watch(metav1.ListOptions{
		ResourceVersion:     rw.lastResourceVersion,
		AllowWatchBookmarks: true,
	})
	// We are very unlikely to hit EOF here since we are just establishing the call,
	// but it may happen that the apiserver is just shutting down (e.g. being restarted)
	// This is consistent with how it is handled for informers
	switch err {
	case nil:
		break

	case io.EOF:
		// watch closed normally
		return false, 0

	case io.ErrUnexpectedEOF:
		klog.V(1).Infof("Watch closed with unexpected EOF: %v", err)
		return false, 0

	default:
		msg := "Watch failed: %v"
		if net.IsProbableEOF(err) || net.IsTimeout(err) {
			klog.V(5).Infof(msg, err)
			// Retry
			return false, 0
		}

		klog.Errorf(msg, err)
		// Retry
		return false, 0
	}

	if watcher == nil {
		klog.Error("Watch returned nil watcher")
		// Retry
		return false, 0
	}

	ch := watcher.ResultChan()
	defer watcher.Stop()

	for {
		select {
		case <-rw.stopChan:
			klog.V(4).Info("Stopping RetryWatcher.")
			return true, 0
		case event, ok := <-ch:
			if !ok {
				klog.V(4).Infof("Failed to get event! Re-creating the watcher. Last RV: %s", rw.lastResourceVersion)
				return false, 0
			}

			// We need to inspect the event and get ResourceVersion out of it
			switch event.Type {
			case watch.Added, watch.Modified, watch.Deleted, watch.Bookmark:
				metaObject, ok := event.Object.(resourceVersionGetter)
				if !ok {
					_ = rw.send(watch.Event{
						Type:   watch.Error,
						Object: &apierrors.NewInternalError(errors.New("retryWatcher: doesn't support resourceVersion")).ErrStatus,
					})
					// We have to abort here because this might cause lastResourceVersion inconsistency by skipping a potential RV with valid data!
					return true, 0
				}

				resourceVersion := metaObject.GetResourceVersion()
				if resourceVersion == "" {
					_ = rw.send(watch.Event{
						Type:   watch.Error,
						Object: &apierrors.NewInternalError(fmt.Errorf("retryWatcher: object %#v doesn't support resourceVersion", event.Object)).ErrStatus,
					})
					// We have to abort here because this might cause lastResourceVersion inconsistency by skipping a potential RV with valid data!
					return true, 0
				}

				// All is fine; send the non-bookmark events and update resource version.
				if event.Type != watch.Bookmark {
					ok = rw.send(event)
					if !ok {
						return true, 0
					}
				}
				rw.lastResourceVersion = resourceVersion

				continue

			case watch.Error:
				// This round trip allows us to handle unstructured status
				errObject := apierrors.FromObject(event.Object)
				statusErr, ok := errObject.(*apierrors.StatusError)
				if !ok {
					klog.Error(spew.Sprintf("Received an error which is not *metav1.Status but %#+v", event.Object))
					// Retry unknown errors
					return false, 0
				}

				status := statusErr.ErrStatus

				statusDelay := time.Duration(0)
				if status.Details != nil {
					statusDelay = time.Duration(status.Details.RetryAfterSeconds) * time.Second
				}

				switch status.Code {
				case http.StatusGone:
					// Never retry RV too old errors
					_ = rw.send(event)
					return true, 0

				case http.StatusGatewayTimeout, http.StatusInternalServerError:
					// Retry
					return false, statusDelay

				default:
					// We retry by default. RetryWatcher is meant to proceed unless it is certain
					// that it can't. If we are not certain, we proceed with retry and leave it
					// up to the user to timeout if needed.

					// Log here so we have a record of hitting the unexpected error
					// and we can whitelist some error codes if we missed any that are expected.
					klog.V(5).Info(spew.Sprintf("Retrying after unexpected error: %#+v", event.Object))

					// Retry
					return false, statusDelay
				}

			default:
				klog.Errorf("Failed to recognize Event type %q", event.Type)
				_ = rw.send(watch.Event{
					Type:   watch.Error,
					Object: &apierrors.NewInternalError(fmt.Errorf("retryWatcher failed to recognize Event type %q", event.Type)).ErrStatus,
				})
				// We are unable to restart the watch and have to stop the loop or this might cause lastResourceVersion inconsistency by skipping a potential RV with valid data!
				return true, 0
			}
		}
	}
}

// receive reads the result from a watcher, restarting it if necessary.
func (rw *RetryWatcher) receive() {
	defer close(rw.doneChan)
	defer close(rw.resultChan)

	klog.V(4).Info("Starting RetryWatcher.")
	defer klog.V(4).Info("Stopping RetryWatcher.")

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	go func() {
		select {
		case <-rw.stopChan:
			cancel()
			return
		case <-ctx.Done():
			return
		}
	}()

	// We use non sliding until so we don't introduce delays on happy path when WATCH call
	// timeouts or gets closed and we need to reestablish it while also avoiding hot loops.
	wait.NonSlidingUntilWithContext(ctx, func(ctx context.Context) {
		done, retryAfter := rw.doReceive()
		if done {
			cancel()
			return
		}

		time.Sleep(retryAfter)

		klog.V(4).Infof("Restarting RetryWatcher at RV=%q", rw.lastResourceVersion)
	}, rw.minRestartDelay)
}

// ResultChan implements Interface.
func (rw *RetryWatcher) ResultChan() <-chan watch.Event {
	return rw.resultChan
}

// Stop implements Interface.
func (rw *RetryWatcher) Stop() {
	close(rw.stopChan)
}

// Done allows the caller to be notified when Retry watcher stops.
func (rw *RetryWatcher) Done() <-chan struct{} {
	return rw.doneChan
}
<commit_msg>Migrate client-go retry-watcher to structured logging<commit_after>/*
Copyright 2017 The Kubernetes Authors.

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.
*/

package watch

import (
	"context"
	"errors"
	"fmt"
	"io"
	"net/http"
	"time"

	"github.com/davecgh/go-spew/spew"

	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/util/net"
	"k8s.io/apimachinery/pkg/util/wait"
	"k8s.io/apimachinery/pkg/watch"
	"k8s.io/client-go/tools/cache"
	"k8s.io/klog/v2"
)

// resourceVersionGetter is an interface used to get resource version from events.
// We can't reuse an interface from meta otherwise it would be a cyclic dependency and we need just this one method
type resourceVersionGetter interface {
	GetResourceVersion() string
}

// RetryWatcher will make sure that in case the underlying watcher is closed (e.g. due to API timeout or etcd timeout)
// it will get restarted from the last point without the consumer even knowing about it.
// RetryWatcher does that by inspecting events and keeping track of resourceVersion.
// Especially useful when using watch.UntilWithoutRetry where premature termination is causing issues and flakes.
// Please note that this is not resilient to etcd cache not having the resource version anymore - you would need to
// use Informers for that.
type RetryWatcher struct {
	lastResourceVersion string
	watcherClient       cache.Watcher
	resultChan          chan watch.Event
	stopChan            chan struct{}
	doneChan            chan struct{}
	minRestartDelay     time.Duration
}

// NewRetryWatcher creates a new RetryWatcher.
// It will make sure that watches gets restarted in case of recoverable errors.
// The initialResourceVersion will be given to watch method when first called.
func NewRetryWatcher(initialResourceVersion string, watcherClient cache.Watcher) (*RetryWatcher, error) {
	return newRetryWatcher(initialResourceVersion, watcherClient, 1*time.Second)
}

func newRetryWatcher(initialResourceVersion string, watcherClient cache.Watcher, minRestartDelay time.Duration) (*RetryWatcher, error) {
	switch initialResourceVersion {
	case "", "0":
		// TODO: revisit this if we ever get WATCH v2 where it means start "now"
		//       without doing the synthetic list of objects at the beginning (see #74022)
		return nil, fmt.Errorf("initial RV %q is not supported due to issues with underlying WATCH", initialResourceVersion)
	default:
		break
	}

	rw := &RetryWatcher{
		lastResourceVersion: initialResourceVersion,
		watcherClient:       watcherClient,
		stopChan:            make(chan struct{}),
		doneChan:            make(chan struct{}),
		resultChan:          make(chan watch.Event, 0),
		minRestartDelay:     minRestartDelay,
	}

	go rw.receive()
	return rw, nil
}

func (rw *RetryWatcher) send(event watch.Event) bool {
	// Writing to an unbuffered channel is blocking operation
	// and we need to check if stop wasn't requested while doing so.
	select {
	case rw.resultChan <- event:
		return true
	case <-rw.stopChan:
		return false
	}
}

// doReceive returns true when it is done, false otherwise.
// If it is not done the second return value holds the time to wait before calling it again.
func (rw *RetryWatcher) doReceive() (bool, time.Duration) {
	watcher, err := rw.watcherClient.Watch(metav1.ListOptions{
		ResourceVersion:     rw.lastResourceVersion,
		AllowWatchBookmarks: true,
	})
	// We are very unlikely to hit EOF here since we are just establishing the call,
	// but it may happen that the apiserver is just shutting down (e.g. being restarted)
	// This is consistent with how it is handled for informers
	switch err {
	case nil:
		break

	case io.EOF:
		// watch closed normally
		return false, 0

	case io.ErrUnexpectedEOF:
		klog.V(1).InfoS("Watch closed with unexpected EOF", "err", err)
		return false, 0

	default:
		msg := "Watch failed"
		if net.IsProbableEOF(err) || net.IsTimeout(err) {
			klog.V(5).InfoS(msg, "err", err)
			// Retry
			return false, 0
		}

		klog.ErrorS(err, msg)
		// Retry
		return false, 0
	}

	if watcher == nil {
		klog.ErrorS(nil, "Watch returned nil watcher")
		// Retry
		return false, 0
	}

	ch := watcher.ResultChan()
	defer watcher.Stop()

	for {
		select {
		case <-rw.stopChan:
			klog.V(4).InfoS("Stopping RetryWatcher.")
			return true, 0
		case event, ok := <-ch:
			if !ok {
				klog.V(4).InfoS("Failed to get event! Re-creating the watcher.", "resourceVersion", rw.lastResourceVersion)
				return false, 0
			}

			// We need to inspect the event and get ResourceVersion out of it
			switch event.Type {
			case watch.Added, watch.Modified, watch.Deleted, watch.Bookmark:
				metaObject, ok := event.Object.(resourceVersionGetter)
				if !ok {
					_ = rw.send(watch.Event{
						Type:   watch.Error,
						Object: &apierrors.NewInternalError(errors.New("retryWatcher: doesn't support resourceVersion")).ErrStatus,
					})
					// We have to abort here because this might cause lastResourceVersion inconsistency by skipping a potential RV with valid data!
					return true, 0
				}

				resourceVersion := metaObject.GetResourceVersion()
				if resourceVersion == "" {
					_ = rw.send(watch.Event{
						Type:   watch.Error,
						Object: &apierrors.NewInternalError(fmt.Errorf("retryWatcher: object %#v doesn't support resourceVersion", event.Object)).ErrStatus,
					})
					// We have to abort here because this might cause lastResourceVersion inconsistency by skipping a potential RV with valid data!
					return true, 0
				}

				// All is fine; send the non-bookmark events and update resource version.
				if event.Type != watch.Bookmark {
					ok = rw.send(event)
					if !ok {
						return true, 0
					}
				}
				rw.lastResourceVersion = resourceVersion

				continue

			case watch.Error:
				// This round trip allows us to handle unstructured status
				errObject := apierrors.FromObject(event.Object)
				statusErr, ok := errObject.(*apierrors.StatusError)
				if !ok {
					klog.Error(spew.Sprintf("Received an error which is not *metav1.Status but %#+v", event.Object))
					// Retry unknown errors
					return false, 0
				}

				status := statusErr.ErrStatus

				statusDelay := time.Duration(0)
				if status.Details != nil {
					statusDelay = time.Duration(status.Details.RetryAfterSeconds) * time.Second
				}

				switch status.Code {
				case http.StatusGone:
					// Never retry RV too old errors
					_ = rw.send(event)
					return true, 0

				case http.StatusGatewayTimeout, http.StatusInternalServerError:
					// Retry
					return false, statusDelay

				default:
					// We retry by default. RetryWatcher is meant to proceed unless it is certain
					// that it can't. If we are not certain, we proceed with retry and leave it
					// up to the user to timeout if needed.

					// Log here so we have a record of hitting the unexpected error
					// and we can whitelist some error codes if we missed any that are expected.
					klog.V(5).Info(spew.Sprintf("Retrying after unexpected error: %#+v", event.Object))

					// Retry
					return false, statusDelay
				}

			default:
				klog.Errorf("Failed to recognize Event type %q", event.Type)
				_ = rw.send(watch.Event{
					Type:   watch.Error,
					Object: &apierrors.NewInternalError(fmt.Errorf("retryWatcher failed to recognize Event type %q", event.Type)).ErrStatus,
				})
				// We are unable to restart the watch and have to stop the loop or this might cause lastResourceVersion inconsistency by skipping a potential RV with valid data!
				return true, 0
			}
		}
	}
}

// receive reads the result from a watcher, restarting it if necessary.
func (rw *RetryWatcher) receive() {
	defer close(rw.doneChan)
	defer close(rw.resultChan)

	klog.V(4).Info("Starting RetryWatcher.")
	defer klog.V(4).Info("Stopping RetryWatcher.")

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	go func() {
		select {
		case <-rw.stopChan:
			cancel()
			return
		case <-ctx.Done():
			return
		}
	}()

	// We use non sliding until so we don't introduce delays on happy path when WATCH call
	// timeouts or gets closed and we need to reestablish it while also avoiding hot loops.
	wait.NonSlidingUntilWithContext(ctx, func(ctx context.Context) {
		done, retryAfter := rw.doReceive()
		if done {
			cancel()
			return
		}

		time.Sleep(retryAfter)

		klog.V(4).Infof("Restarting RetryWatcher at RV=%q", rw.lastResourceVersion)
	}, rw.minRestartDelay)
}

// ResultChan implements Interface.
func (rw *RetryWatcher) ResultChan() <-chan watch.Event {
	return rw.resultChan
}

// Stop implements Interface.
func (rw *RetryWatcher) Stop() {
	close(rw.stopChan)
}

// Done allows the caller to be notified when Retry watcher stops.
func (rw *RetryWatcher) Done() <-chan struct{} {
	return rw.doneChan
}
<|endoftext|>


<commit_before>package activitypub

import (
	"errors"
	as "github.com/go-ap/activitystreams"
)

// Endpoints a json object which maps additional (typically server/domain-wide)
// endpoints which may be useful either for this actor or someone referencing this actor.
// This mapping may be nested inside the actor document as the value or may be a link to
// a JSON-LD document with these properties.
type Endpoints struct {
	// UploadMedia Upload endpoint URI for this user for binary data.
	UploadMedia as.Item `jsonld:"uploadMedia,omitempty"`
	// OauthAuthorizationEndpoint Endpoint URI so this actor's clients may access remote ActivityStreams objects which require authentication
	// to access. To use this endpoint, the client posts an x-www-form-urlencoded id parameter with the value being
	// the id of the requested ActivityStreams object.
	OauthAuthorizationEndpoint as.Item `jsonld:"oauthAuthorizationEndpoint,omitempty"`
	// OauthTokenEndpoint If OAuth 2.0 bearer tokens [RFC6749] [RFC6750] are being used for authenticating client to server interactions,
	// this endpoint specifies a URI at which a browser-authenticated user may obtain a new authorization grant.
	OauthTokenEndpoint as.Item `jsonld:"oauthTokenEndpoint,omitempty"`
	// ProvideClientKey  If OAuth 2.0 bearer tokens [RFC6749] [RFC6750] are being used for authenticating client to server interactions,
	// this endpoint specifies a URI at which a client may acquire an access token.
	ProvideClientKey as.Item `jsonld:"provideClientKey,omitempty"`
	// SignClientKey If Linked Data Signatures and HTTP Signatures are being used for authentication and authorization,
	// this endpoint specifies a URI at which browser-authenticated users may authorize a client's public
	// key for client to server interactions.
	SignClientKey as.Item `jsonld:"signClientKey,omitempty"`
	// SharedInbox If Linked Data Signatures and HTTP Signatures are being used for authentication and authorization,
	// this endpoint specifies a URI at which a client key may be signed by the actor's key for a time window to
	// act on behalf of the actor in interacting with foreign servers.
	SharedInbox as.Item `jsonld:"sharedInbox,omitempty"`
}

// Actor is generally one of the ActivityStreams actor Types, but they don't have to be.
// For example, a Profile object might be used as an actor, or a type from an ActivityStreams extension.
// Actors are retrieved like any other Object in ActivityPub.
// Like other ActivityStreams objects, actors have an id, which is a URI.
type actor struct {
	Parent
	// A reference to an [ActivityStreams] OrderedCollection comprised of all the messages received by the actor;
	// see 5.2 Inbox.
	Inbox as.Item `jsonld:"inbox,omitempty"`
	// An [ActivityStreams] OrderedCollection comprised of all the messages produced by the actor;
	// see 5.1 Outbox.
	Outbox as.Item `jsonld:"outbox,omitempty"`
	// A link to an [ActivityStreams] collection of the actors that this actor is following;
	// see 5.4 Following Collection
	Following as.Item `jsonld:"following,omitempty"`
	// A link to an [ActivityStreams] collection of the actors that follow this actor;
	// see 5.3 Followers Collection.
	Followers as.Item `jsonld:"followers,omitempty"`
	// A link to an [ActivityStreams] collection of objects this actor has liked;
	// see 5.5 Liked Collection.
	Liked as.Item `jsonld:"liked,omitempty"`
	// A short username which may be used to refer to the actor, with no uniqueness guarantees.
	PreferredUsername as.NaturalLanguageValues `jsonld:"preferredUsername,omitempty,collapsible"`
	// A json object which maps additional (typically server/domain-wide) endpoints which may be useful either
	// for this actor or someone referencing this actor.
	// This mapping may be nested inside the actor document as the value or may be a link
	// to a JSON-LD document with these properties.
	Endpoints *Endpoints `jsonld:"endpoints,omitempty"`
	// A list of supplementary Collections which may be of interest.
	Streams []as.ItemCollection `jsonld:"streams,omitempty"`
}

type (
	// Application describes a software application.
	Application = actor

	// Group represents a formal or informal collective of Actors.
	Group = actor

	// Organization represents an organization.
	Organization = actor

	// Person represents an individual person.
	Person = actor

	// Service represents a service of any kind.
	Service = actor
)

// actorNew initializes an actor type actor
func actorNew(id as.ObjectID, typ as.ActivityVocabularyType) *actor {
	if !as.ActorTypes.Contains(typ) {
		typ = as.ActorType
	}

	a := actor{Parent: Object{Parent: as.Parent{ID: id, Type: typ}}}
	a.Name = as.NaturalLanguageValuesNew()
	a.Content = as.NaturalLanguageValuesNew()
	a.Summary = as.NaturalLanguageValuesNew()
	in := as.OrderedCollectionNew(as.ObjectID("test-inbox"))
	out := as.OrderedCollectionNew(as.ObjectID("test-outbox"))
	liked := as.OrderedCollectionNew(as.ObjectID("test-liked"))

	a.Inbox = in
	a.Outbox = out
	a.Liked = liked
	a.PreferredUsername = as.NaturalLanguageValuesNew()

	return &a
}

// ApplicationNew initializes an Application type actor
func ApplicationNew(id as.ObjectID) *Application {
	return actorNew(id, as.ApplicationType)
}

// GroupNew initializes a Group type actor
func GroupNew(id as.ObjectID) *Group {
	return actorNew(id, as.GroupType)
}

// OrganizationNew initializes an Organization type actor
func OrganizationNew(id as.ObjectID) *Organization {
	return actorNew(id, as.OrganizationType)
}

// PersonNew initializes a Person type actor
func PersonNew(id as.ObjectID) *Person {
	return actorNew(id, as.PersonType)
}

// ServiceNew initializes a Service type actor
func ServiceNew(id as.ObjectID) *Service {
	return actorNew(id, as.ServiceType)
}

func (a *actor) UnmarshalJSON(data []byte) error {
	if as.ItemTyperFunc == nil {
		as.ItemTyperFunc = JSONGetItemByType
	}
	a.Parent.UnmarshalJSON(data)
	a.PreferredUsername = as.JSONGetNaturalLanguageField(data, "preferredUsername")
	a.Followers = as.JSONGetItem(data, "followers")
	a.Following = as.JSONGetItem(data, "following")
	a.Inbox = as.JSONGetItem(data, "inbox")
	a.Outbox = as.JSONGetItem(data, "outbox")
	a.Liked = as.JSONGetItem(data, "liked")
	a.Endpoints = JSONGetActorEndpoints(data, "endpoints")
	// TODO(marius): Streams needs custom unmarshalling
	//a.Streams = as.JSONGetItems(data, "streams")
	return nil
}

// ToPerson
func ToPerson(it as.Item) (*Person, error) {
	switch i := it.(type) {
	case *as.Object:
		return &Person{Parent: Object{Parent: *i}}, nil
	case as.Object:
		return &Person{Parent: Object{Parent: i}}, nil
	case *Object:
		return &Person{Parent: *i}, nil
	case Object:
		return &Person{Parent: i}, nil
	case *actor:
		return i, nil
	case actor:
		return &i, nil
	}
	return nil, errors.New("unable to convert object")
}

// UnmarshalJSON
func (e *Endpoints) UnmarshalJSON(data []byte) error {
	e.OauthAuthorizationEndpoint = as.JSONGetItem(data, "oauthAuthorizationEndpoint")
	e.OauthTokenEndpoint = as.JSONGetItem(data, "oauthTokenEndpoint")
	e.UploadMedia = as.JSONGetItem(data, "uploadMedia")
	e.ProvideClientKey = as.JSONGetItem(data, "provideClientKey")
	e.SignClientKey = as.JSONGetItem(data, "signClientKey")
	e.SharedInbox = as.JSONGetItem(data, "sharedInbox")
	return nil
}
<commit_msg>Fixed sharedInbox documenation<commit_after>package activitypub

import (
	"errors"
	as "github.com/go-ap/activitystreams"
)

// Endpoints a json object which maps additional (typically server/domain-wide)
// endpoints which may be useful either for this actor or someone referencing this actor.
// This mapping may be nested inside the actor document as the value or may be a link to
// a JSON-LD document with these properties.
type Endpoints struct {
	// UploadMedia Upload endpoint URI for this user for binary data.
	UploadMedia as.Item `jsonld:"uploadMedia,omitempty"`
	// OauthAuthorizationEndpoint Endpoint URI so this actor's clients may access remote ActivityStreams objects which require authentication
	// to access. To use this endpoint, the client posts an x-www-form-urlencoded id parameter with the value being
	// the id of the requested ActivityStreams object.
	OauthAuthorizationEndpoint as.Item `jsonld:"oauthAuthorizationEndpoint,omitempty"`
	// OauthTokenEndpoint If OAuth 2.0 bearer tokens [RFC6749] [RFC6750] are being used for authenticating client to server interactions,
	// this endpoint specifies a URI at which a browser-authenticated user may obtain a new authorization grant.
	OauthTokenEndpoint as.Item `jsonld:"oauthTokenEndpoint,omitempty"`
	// ProvideClientKey  If OAuth 2.0 bearer tokens [RFC6749] [RFC6750] are being used for authenticating client to server interactions,
	// this endpoint specifies a URI at which a client may acquire an access token.
	ProvideClientKey as.Item `jsonld:"provideClientKey,omitempty"`
	// SignClientKey If Linked Data Signatures and HTTP Signatures are being used for authentication and authorization,
	// this endpoint specifies a URI at which browser-authenticated users may authorize a client's public
	// key for client to server interactions.
	SignClientKey as.Item `jsonld:"signClientKey,omitempty"`
	// SharedInbox An optional endpoint used for wide delivery of publicly addressed activities and activities sent to followers.
	// SharedInbox endpoints SHOULD also be publicly readable OrderedCollection objects containing objects addressed to the
	// Public special collection. Reading from the sharedInbox endpoint MUST NOT present objects which are not addressed to the Public endpoint.
	SharedInbox as.Item `jsonld:"sharedInbox,omitempty"`
}

// Actor is generally one of the ActivityStreams actor Types, but they don't have to be.
// For example, a Profile object might be used as an actor, or a type from an ActivityStreams extension.
// Actors are retrieved like any other Object in ActivityPub.
// Like other ActivityStreams objects, actors have an id, which is a URI.
type actor struct {
	Parent
	// A reference to an [ActivityStreams] OrderedCollection comprised of all the messages received by the actor;
	// see 5.2 Inbox.
	Inbox as.Item `jsonld:"inbox,omitempty"`
	// An [ActivityStreams] OrderedCollection comprised of all the messages produced by the actor;
	// see 5.1 Outbox.
	Outbox as.Item `jsonld:"outbox,omitempty"`
	// A link to an [ActivityStreams] collection of the actors that this actor is following;
	// see 5.4 Following Collection
	Following as.Item `jsonld:"following,omitempty"`
	// A link to an [ActivityStreams] collection of the actors that follow this actor;
	// see 5.3 Followers Collection.
	Followers as.Item `jsonld:"followers,omitempty"`
	// A link to an [ActivityStreams] collection of objects this actor has liked;
	// see 5.5 Liked Collection.
	Liked as.Item `jsonld:"liked,omitempty"`
	// A short username which may be used to refer to the actor, with no uniqueness guarantees.
	PreferredUsername as.NaturalLanguageValues `jsonld:"preferredUsername,omitempty,collapsible"`
	// A json object which maps additional (typically server/domain-wide) endpoints which may be useful either
	// for this actor or someone referencing this actor.
	// This mapping may be nested inside the actor document as the value or may be a link
	// to a JSON-LD document with these properties.
	Endpoints *Endpoints `jsonld:"endpoints,omitempty"`
	// A list of supplementary Collections which may be of interest.
	Streams []as.ItemCollection `jsonld:"streams,omitempty"`
}

type (
	// Application describes a software application.
	Application = actor

	// Group represents a formal or informal collective of Actors.
	Group = actor

	// Organization represents an organization.
	Organization = actor

	// Person represents an individual person.
	Person = actor

	// Service represents a service of any kind.
	Service = actor
)

// actorNew initializes an actor type actor
func actorNew(id as.ObjectID, typ as.ActivityVocabularyType) *actor {
	if !as.ActorTypes.Contains(typ) {
		typ = as.ActorType
	}

	a := actor{Parent: Object{Parent: as.Parent{ID: id, Type: typ}}}
	a.Name = as.NaturalLanguageValuesNew()
	a.Content = as.NaturalLanguageValuesNew()
	a.Summary = as.NaturalLanguageValuesNew()
	in := as.OrderedCollectionNew(as.ObjectID("test-inbox"))
	out := as.OrderedCollectionNew(as.ObjectID("test-outbox"))
	liked := as.OrderedCollectionNew(as.ObjectID("test-liked"))

	a.Inbox = in
	a.Outbox = out
	a.Liked = liked
	a.PreferredUsername = as.NaturalLanguageValuesNew()

	return &a
}

// ApplicationNew initializes an Application type actor
func ApplicationNew(id as.ObjectID) *Application {
	return actorNew(id, as.ApplicationType)
}

// GroupNew initializes a Group type actor
func GroupNew(id as.ObjectID) *Group {
	return actorNew(id, as.GroupType)
}

// OrganizationNew initializes an Organization type actor
func OrganizationNew(id as.ObjectID) *Organization {
	return actorNew(id, as.OrganizationType)
}

// PersonNew initializes a Person type actor
func PersonNew(id as.ObjectID) *Person {
	return actorNew(id, as.PersonType)
}

// ServiceNew initializes a Service type actor
func ServiceNew(id as.ObjectID) *Service {
	return actorNew(id, as.ServiceType)
}

func (a *actor) UnmarshalJSON(data []byte) error {
	if as.ItemTyperFunc == nil {
		as.ItemTyperFunc = JSONGetItemByType
	}
	a.Parent.UnmarshalJSON(data)
	a.PreferredUsername = as.JSONGetNaturalLanguageField(data, "preferredUsername")
	a.Followers = as.JSONGetItem(data, "followers")
	a.Following = as.JSONGetItem(data, "following")
	a.Inbox = as.JSONGetItem(data, "inbox")
	a.Outbox = as.JSONGetItem(data, "outbox")
	a.Liked = as.JSONGetItem(data, "liked")
	a.Endpoints = JSONGetActorEndpoints(data, "endpoints")
	// TODO(marius): Streams needs custom unmarshalling
	//a.Streams = as.JSONGetItems(data, "streams")
	return nil
}

// ToPerson
func ToPerson(it as.Item) (*Person, error) {
	switch i := it.(type) {
	case *as.Object:
		return &Person{Parent: Object{Parent: *i}}, nil
	case as.Object:
		return &Person{Parent: Object{Parent: i}}, nil
	case *Object:
		return &Person{Parent: *i}, nil
	case Object:
		return &Person{Parent: i}, nil
	case *actor:
		return i, nil
	case actor:
		return &i, nil
	}
	return nil, errors.New("unable to convert object")
}

// UnmarshalJSON
func (e *Endpoints) UnmarshalJSON(data []byte) error {
	e.OauthAuthorizationEndpoint = as.JSONGetItem(data, "oauthAuthorizationEndpoint")
	e.OauthTokenEndpoint = as.JSONGetItem(data, "oauthTokenEndpoint")
	e.UploadMedia = as.JSONGetItem(data, "uploadMedia")
	e.ProvideClientKey = as.JSONGetItem(data, "provideClientKey")
	e.SignClientKey = as.JSONGetItem(data, "signClientKey")
	e.SharedInbox = as.JSONGetItem(data, "sharedInbox")
	return nil
}
<|endoftext|>