├── .gitignore ├── LICENSE ├── archive ├── archive.go ├── tar.go └── zip.go ├── binary ├── writer.go └── writer_test.go ├── bits ├── hamming.go ├── hamming_test.go ├── parity.go └── parity_test.go ├── byte ├── iterator.go ├── iterator_test.go ├── length.go ├── length_test.go ├── pad.go └── pad_test.go ├── config └── config.go ├── context ├── canceller.go └── canceller_test.go ├── debug ├── stack.go └── stack_test.go ├── defer ├── closer.go └── closer_test.go ├── error └── multiple.go ├── event └── dispatcher.go ├── exec ├── browser.go ├── cmd.go ├── cmd_test.go ├── writer.go └── writer_test.go ├── flag ├── flag.go ├── flag_test.go ├── time.go └── time_test.go ├── float ├── rational.go └── rational_test.go ├── go.mod ├── go.sum ├── http ├── downloader.go ├── downloader_test.go ├── http.go ├── middleware.go ├── sender.go └── server.go ├── image ├── color.go └── color_test.go ├── io ├── copy.go ├── copy_test.go ├── linearizer.go ├── linearizer_test.go └── reader.go ├── limiter ├── bucket.go ├── bucket_test.go └── limiter.go ├── main.go ├── map └── map.go ├── os ├── checksum.go ├── checksum_test.go ├── copy.go ├── copy_test.go ├── dir.go ├── file.go ├── move.go ├── signals.go └── testdata │ ├── checksum │ └── copy │ ├── d │ ├── d1 │ │ └── f11 │ ├── d2 │ │ ├── d21 │ │ │ └── f211 │ │ └── f21 │ └── f1 │ └── f ├── pcm ├── amplitude.go ├── amplitude_test.go ├── bit_depth.go ├── bit_depth_test.go ├── channels.go ├── channels_test.go ├── level.go ├── level_test.go ├── sample_rate.go ├── sample_rate_test.go ├── silence.go └── silence_test.go ├── ptr └── astiptr.go ├── readme.md ├── regexp ├── replace.go └── replace_test.go ├── slice ├── slice.go └── slice_test.go ├── sort ├── int64.go ├── uint16.go └── uint8.go ├── ssh ├── auth.go └── copy.go ├── stat ├── duration.go ├── increment.go └── stater.go ├── string ├── length.go ├── length_test.go └── rand.go ├── sync ├── chan.go ├── ctx_queue.go ├── mutex.go └── mutex_test.go ├── template ├── template.go ├── template_test.go ├── templater.go └── tests │ ├── subdir │ └── template_4.html │ ├── template_1.html │ ├── template_2.css │ └── template_3.html ├── time ├── sleep.go ├── sleep_test.go ├── timestamp.go └── timestamp_test.go ├── unicode ├── reader.go ├── utf8.go └── utf8_test.go ├── url └── url.go └── worker ├── README.md ├── amqp.go ├── dial.go ├── executer.go ├── server.go ├── task.go └── worker.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Quentin Renard 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /archive/archive.go: -------------------------------------------------------------------------------- 1 | package astiarchive 2 | 3 | import "os" 4 | 5 | // DefaultFileMode represents the default file mode 6 | var DefaultFileMode os.FileMode = 0775 7 | -------------------------------------------------------------------------------- /archive/tar.go: -------------------------------------------------------------------------------- 1 | package astiarchive 2 | 3 | import ( 4 | "archive/tar" 5 | "compress/gzip" 6 | "context" 7 | "os" 8 | 9 | "io" 10 | 11 | "path/filepath" 12 | 13 | "github.com/asticode/go-astitools/io" 14 | "github.com/pkg/errors" 15 | ) 16 | 17 | // Untar untars a src into a dst 18 | func Untar(ctx context.Context, src, dst string) (err error) { 19 | // Open src 20 | var srcFile *os.File 21 | if srcFile, err = os.Open(src); err != nil { 22 | return errors.Wrapf(err, "astiarchive: opening %s failed", src) 23 | } 24 | defer srcFile.Close() 25 | 26 | // Create gzip reader 27 | var gzr *gzip.Reader 28 | if gzr, err = gzip.NewReader(srcFile); err != nil { 29 | return errors.Wrap(err, "astiarchive: creating gzip reader failed") 30 | } 31 | defer gzr.Close() 32 | 33 | // Loop through tar entries 34 | tr := tar.NewReader(gzr) 35 | for { 36 | // Get next entry 37 | var h *tar.Header 38 | if h, err = tr.Next(); err != nil { 39 | if err == io.EOF { 40 | err = nil 41 | break 42 | } 43 | return errors.Wrap(err, "astiarchive: getting next header failed") 44 | } 45 | 46 | // No header 47 | if h == nil { 48 | continue 49 | } 50 | 51 | // Build path 52 | p := filepath.Join(dst, h.Name) 53 | 54 | // Switch on file type 55 | switch h.Typeflag { 56 | case tar.TypeDir: 57 | if err = os.MkdirAll(p, h.FileInfo().Mode().Perm()); err != nil { 58 | err = errors.Wrapf(err, "astiarchive: mkdirall %s failed", p) 59 | return 60 | } 61 | case tar.TypeReg: 62 | if err = createTarFile(ctx, p, h, tr); err != nil { 63 | err = errors.Wrapf(err, "astiarchive: creating tar file into %s failed", p) 64 | return 65 | } 66 | } 67 | } 68 | return 69 | } 70 | 71 | func createTarFile(ctx context.Context, p string, h *tar.Header, tr *tar.Reader) (err error) { 72 | // Sometimes the dir that will contain the file has not yet been processed in the tar ball, therefore we need to create it 73 | if err = os.MkdirAll(filepath.Dir(p), DefaultFileMode); err != nil { 74 | err = errors.Wrapf(err, "astiarchive: mkdirall %s failed", filepath.Dir(p)) 75 | return 76 | } 77 | 78 | // Open file 79 | var f *os.File 80 | if f, err = os.OpenFile(p, os.O_TRUNC|os.O_CREATE|os.O_RDWR, h.FileInfo().Mode().Perm()); err != nil { 81 | err = errors.Wrap(err, "astiarchive: opening file failed") 82 | return 83 | } 84 | defer f.Close() 85 | 86 | // Copy 87 | if _, err = astiio.Copy(ctx, tr, f); err != nil { 88 | err = errors.Wrap(err, "astiarchive: copying content failed") 89 | return 90 | } 91 | return 92 | } 93 | -------------------------------------------------------------------------------- /archive/zip.go: -------------------------------------------------------------------------------- 1 | package astiarchive 2 | 3 | import ( 4 | "archive/zip" 5 | "context" 6 | "io" 7 | "io/ioutil" 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | 12 | "github.com/asticode/go-astitools/io" 13 | "github.com/pkg/errors" 14 | ) 15 | 16 | // Zip zips a src into a dst 17 | // dstRoot can be used to create root directories so that the content is zipped in /path/to/zip.zip/root/path 18 | func Zip(ctx context.Context, src, dst, dstRoot string) (err error) { 19 | // Create destination file 20 | var dstFile *os.File 21 | if dstFile, err = os.Create(dst); err != nil { 22 | return err 23 | } 24 | defer dstFile.Close() 25 | 26 | // Create zip writer 27 | var zw = zip.NewWriter(dstFile) 28 | defer zw.Close() 29 | 30 | // Walk 31 | filepath.Walk(src, func(path string, info os.FileInfo, e1 error) (e2 error) { 32 | // Process error 33 | if e1 != nil { 34 | return e1 35 | } 36 | 37 | // Init header 38 | var h *zip.FileHeader 39 | if h, e2 = zip.FileInfoHeader(info); e2 != nil { 40 | return 41 | } 42 | 43 | // Set header info 44 | h.Name = filepath.Join(dstRoot, strings.TrimPrefix(path, src)) 45 | if info.IsDir() { 46 | h.Name += "/" 47 | } else { 48 | h.Method = zip.Deflate 49 | } 50 | 51 | // Create writer 52 | var w io.Writer 53 | if w, e2 = zw.CreateHeader(h); e2 != nil { 54 | return 55 | } 56 | 57 | // If path is dir, stop here 58 | if info.IsDir() { 59 | return 60 | } 61 | 62 | // Open path 63 | var walkFile *os.File 64 | if walkFile, e2 = os.Open(path); e2 != nil { 65 | return 66 | } 67 | defer walkFile.Close() 68 | 69 | // Copy 70 | if _, e2 = astiio.Copy(ctx, walkFile, w); e2 != nil { 71 | return 72 | } 73 | return 74 | }) 75 | return 76 | } 77 | 78 | // Unzip unzips a src into a dst 79 | // Possible src formats are /path/to/zip.zip or /path/to/zip.zip/internal/path if you only want to unzip files in 80 | // /internal/path in the .zip archive 81 | func Unzip(ctx context.Context, src, dst string) (err error) { 82 | // Parse src path 83 | var split = strings.Split(src, ".zip") 84 | src = split[0] + ".zip" 85 | var internalPath string 86 | if len(split) >= 2 { 87 | internalPath = split[1] 88 | } 89 | 90 | // Open overall reader 91 | var r *zip.ReadCloser 92 | if r, err = zip.OpenReader(src); err != nil { 93 | return errors.Wrapf(err, "astiarchive: opening overall zip reader on %s failed", src) 94 | } 95 | defer r.Close() 96 | 97 | // Loop through files to determine their type 98 | var dirs, files, symlinks = make(map[string]*zip.File), make(map[string]*zip.File), make(map[string]*zip.File) 99 | for _, f := range r.File { 100 | // Validate internal path 101 | var n = string(os.PathSeparator) + f.Name 102 | if internalPath != "" && !strings.HasPrefix(n, internalPath) { 103 | continue 104 | } 105 | var p = filepath.Join(dst, strings.TrimPrefix(n, internalPath)) 106 | 107 | // Check file type 108 | if f.FileInfo().Mode()&os.ModeSymlink != 0 { 109 | symlinks[p] = f 110 | } else if f.FileInfo().IsDir() { 111 | dirs[p] = f 112 | } else { 113 | files[p] = f 114 | } 115 | } 116 | 117 | // Create dirs 118 | for p, f := range dirs { 119 | if err = os.MkdirAll(p, f.FileInfo().Mode().Perm()); err != nil { 120 | return errors.Wrapf(err, "astiarchive: mkdirall %s failed", p) 121 | } 122 | } 123 | 124 | // Create files 125 | for p, f := range files { 126 | if err = createZipFile(ctx, f, p); err != nil { 127 | return errors.Wrapf(err, "astiarchive: creating zip file into %s failed", p) 128 | } 129 | } 130 | 131 | // Create symlinks 132 | for p, f := range symlinks { 133 | if err = createZipSymlink(f, p); err != nil { 134 | return errors.Wrapf(err, "astiarchive: creating zip symlink into %s failed", p) 135 | } 136 | } 137 | return 138 | } 139 | 140 | func createZipFile(ctx context.Context, f *zip.File, p string) (err error) { 141 | // Open file reader 142 | var fr io.ReadCloser 143 | if fr, err = f.Open(); err != nil { 144 | return errors.Wrapf(err, "astiarchive: opening zip reader on file %s failed", f.Name) 145 | } 146 | defer fr.Close() 147 | 148 | // Since dirs don't always come up we make sure the directory of the file exists with default 149 | // file mode 150 | if err = os.MkdirAll(filepath.Dir(p), DefaultFileMode); err != nil { 151 | return errors.Wrapf(err, "astiarchive: mkdirall %s failed", filepath.Dir(p)) 152 | } 153 | 154 | // Open the file 155 | var fl *os.File 156 | if fl, err = os.OpenFile(p, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.FileInfo().Mode().Perm()); err != nil { 157 | return errors.Wrapf(err, "astiarchive: opening file %s failed", p) 158 | } 159 | defer fl.Close() 160 | 161 | // Copy 162 | if _, err = astiio.Copy(ctx, fr, fl); err != nil { 163 | return errors.Wrapf(err, "astiarchive: copying %s into %s failed", f.Name, p) 164 | } 165 | return 166 | } 167 | 168 | func createZipSymlink(f *zip.File, p string) (err error) { 169 | // Open file reader 170 | var fr io.ReadCloser 171 | if fr, err = f.Open(); err != nil { 172 | return errors.Wrapf(err, "astiarchive: opening zip reader on file %s failed", f.Name) 173 | } 174 | defer fr.Close() 175 | 176 | // If file is a symlink we retrieve the target path that is in the content of the file 177 | var b []byte 178 | if b, err = ioutil.ReadAll(fr); err != nil { 179 | return errors.Wrapf(err, "astiarchive: ioutil.Readall on %s failed", f.Name) 180 | } 181 | 182 | // Create the symlink 183 | if err = os.Symlink(string(b), p); err != nil { 184 | return errors.Wrapf(err, "astiarchive: creating symlink from %s to %s failed", string(b), p) 185 | } 186 | return 187 | } 188 | -------------------------------------------------------------------------------- /binary/writer.go: -------------------------------------------------------------------------------- 1 | package astibinary 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | ) 7 | 8 | // Writer represents a writer 9 | type Writer struct { 10 | b []byte 11 | remainingBits string 12 | } 13 | 14 | // New represents a binary writer 15 | func New() *Writer { 16 | return &Writer{b: []byte{}} 17 | } 18 | 19 | // Bytes returns the writer bytes 20 | func (w *Writer) Bytes() []byte { 21 | return w.b 22 | } 23 | 24 | // Write writes binary stuff 25 | func (w *Writer) Write(i interface{}) { 26 | var s string 27 | switch i.(type) { 28 | case string: 29 | s = i.(string) 30 | case []byte: 31 | for _, b := range i.([]byte) { 32 | s += fmt.Sprintf("%.8b", b) 33 | } 34 | case bool: 35 | if i.(bool) { 36 | s = "1" 37 | } else { 38 | s = "0" 39 | } 40 | case uint8: 41 | s = fmt.Sprintf("%.8b", i) 42 | case uint16: 43 | s = fmt.Sprintf("%.16b", i) 44 | case uint32: 45 | s = fmt.Sprintf("%.32b", i) 46 | case uint64: 47 | s = fmt.Sprintf("%.64b", i) 48 | default: 49 | return 50 | } 51 | 52 | for _, c := range s { 53 | w.remainingBits = w.remainingBits + string(c) 54 | if len(w.remainingBits) == 8 { 55 | var nb byte 56 | var power float64 57 | for idx := len(w.remainingBits) - 1; idx >= 0; idx-- { 58 | if w.remainingBits[idx] == '1' { 59 | nb = nb + uint8(math.Pow(2, power)) 60 | } 61 | power++ 62 | } 63 | w.b = append(w.b, nb) 64 | w.remainingBits = "" 65 | } 66 | } 67 | } 68 | 69 | // Reset resets the writer 70 | func (w *Writer) Reset() { 71 | w.b = []byte{} 72 | w.remainingBits = "" 73 | } 74 | -------------------------------------------------------------------------------- /binary/writer_test.go: -------------------------------------------------------------------------------- 1 | package astibinary 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestBinaryWriter(t *testing.T) { 10 | w := New() 11 | w.Write("1100") 12 | assert.Empty(t, w.Bytes()) 13 | assert.Equal(t, "1100", w.remainingBits) 14 | w.Write("11001") 15 | assert.Equal(t, []byte{0xcc}, w.Bytes()) 16 | assert.Equal(t, "1", w.remainingBits) 17 | w.Reset() 18 | assert.Empty(t, w.Bytes()) 19 | w.Write("11") 20 | w.Write(uint8(170)) 21 | assert.Equal(t, []byte{0xea}, w.Bytes()) 22 | assert.Equal(t, "10", w.remainingBits) 23 | w.Write([]byte{0xcc, 0xcd}) 24 | assert.Equal(t, []byte{0xea, 0xb3, 0x33}, w.Bytes()) 25 | assert.Equal(t, "01", w.remainingBits) 26 | } 27 | -------------------------------------------------------------------------------- /bits/hamming.go: -------------------------------------------------------------------------------- 1 | package astibits 2 | 3 | var hamming84tab = [256]uint8{ 4 | 0x01, 0xff, 0xff, 0x08, 0xff, 0x0c, 0x04, 0xff, 0xff, 0x08, 0x08, 0x08, 0x06, 0xff, 0xff, 0x08, 5 | 0xff, 0x0a, 0x02, 0xff, 0x06, 0xff, 0xff, 0x0f, 0x06, 0xff, 0xff, 0x08, 0x06, 0x06, 0x06, 0xff, 6 | 0xff, 0x0a, 0x04, 0xff, 0x04, 0xff, 0x04, 0x04, 0x00, 0xff, 0xff, 0x08, 0xff, 0x0d, 0x04, 0xff, 7 | 0x0a, 0x0a, 0xff, 0x0a, 0xff, 0x0a, 0x04, 0xff, 0xff, 0x0a, 0x03, 0xff, 0x06, 0xff, 0xff, 0x0e, 8 | 0x01, 0x01, 0x01, 0xff, 0x01, 0xff, 0xff, 0x0f, 0x01, 0xff, 0xff, 0x08, 0xff, 0x0d, 0x05, 0xff, 9 | 0x01, 0xff, 0xff, 0x0f, 0xff, 0x0f, 0x0f, 0x0f, 0xff, 0x0b, 0x03, 0xff, 0x06, 0xff, 0xff, 0x0f, 10 | 0x01, 0xff, 0xff, 0x09, 0xff, 0x0d, 0x04, 0xff, 0xff, 0x0d, 0x03, 0xff, 0x0d, 0x0d, 0xff, 0x0d, 11 | 0xff, 0x0a, 0x03, 0xff, 0x07, 0xff, 0xff, 0x0f, 0x03, 0xff, 0x03, 0x03, 0xff, 0x0d, 0x03, 0xff, 12 | 0xff, 0x0c, 0x02, 0xff, 0x0c, 0x0c, 0xff, 0x0c, 0x00, 0xff, 0xff, 0x08, 0xff, 0x0c, 0x05, 0xff, 13 | 0x02, 0xff, 0x02, 0x02, 0xff, 0x0c, 0x02, 0xff, 0xff, 0x0b, 0x02, 0xff, 0x06, 0xff, 0xff, 0x0e, 14 | 0x00, 0xff, 0xff, 0x09, 0xff, 0x0c, 0x04, 0xff, 0x00, 0x00, 0x00, 0xff, 0x00, 0xff, 0xff, 0x0e, 15 | 0xff, 0x0a, 0x02, 0xff, 0x07, 0xff, 0xff, 0x0e, 0x00, 0xff, 0xff, 0x0e, 0xff, 0x0e, 0x0e, 0x0e, 16 | 0x01, 0xff, 0xff, 0x09, 0xff, 0x0c, 0x05, 0xff, 0xff, 0x0b, 0x05, 0xff, 0x05, 0xff, 0x05, 0x05, 17 | 0xff, 0x0b, 0x02, 0xff, 0x07, 0xff, 0xff, 0x0f, 0x0b, 0x0b, 0xff, 0x0b, 0xff, 0x0b, 0x05, 0xff, 18 | 0xff, 0x09, 0x09, 0x09, 0x07, 0xff, 0xff, 0x09, 0x00, 0xff, 0xff, 0x09, 0xff, 0x0d, 0x05, 0xff, 19 | 0x07, 0xff, 0xff, 0x09, 0x07, 0x07, 0x07, 0xff, 0xff, 0x0b, 0x03, 0xff, 0x07, 0xff, 0xff, 0x0e, 20 | } 21 | 22 | func Hamming84Decode(i uint8) (o uint8, ok bool) { 23 | o = hamming84tab[i] 24 | if o == 0xff { 25 | return 26 | } 27 | ok = true 28 | return 29 | } 30 | 31 | func Hamming2418Decode(i uint32) (o uint32, ok bool) { 32 | o = i 33 | ok = true 34 | return 35 | } 36 | -------------------------------------------------------------------------------- /bits/hamming_test.go: -------------------------------------------------------------------------------- 1 | package astibits 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func testHamming84Decode(i uint8) (o uint8, ok bool) { 10 | p1, d1, p2, d2, p3, d3, p4, d4 := i>>7&0x1, i>>6&0x1, i>>5&0x1, i>>4&0x1, i>>3&0x1, i>>2&0x1, i>>1&0x1, i&0x1 11 | testA := p1^d1^d3^d4 > 0 12 | testB := d1^p2^d2^d4 > 0 13 | testC := d1^d2^p3^d3 > 0 14 | testD := p1^d1^p2^d2^p3^d3^p4^d4 > 0 15 | if testA && testB && testC { 16 | // p4 may be incorrect 17 | } else if testD && (!testA || !testB || !testC) { 18 | return 19 | } else { 20 | if !testA && testB && testC { 21 | // p1 is incorrect 22 | } else if testA && !testB && testC { 23 | // p2 is incorrect 24 | } else if testA && testB && !testC { 25 | // p3 is incorrect 26 | } else if !testA && !testB && testC { 27 | // d4 is incorrect 28 | d4 ^= 1 29 | } else if testA && !testB && !testC { 30 | // d2 is incorrect 31 | d2 ^= 1 32 | } else if !testA && testB && !testC { 33 | // d3 is incorrect 34 | d3 ^= 1 35 | } else { 36 | // d1 is incorrect 37 | d1 ^= 1 38 | } 39 | } 40 | o = uint8(d4<<3 | d3<<2 | d2<<1 | d1) 41 | ok = true 42 | return 43 | } 44 | 45 | func TestHamming84Decode(t *testing.T) { 46 | for i := 0; i < 256; i++ { 47 | v, okV := Hamming84Decode(uint8(i)) 48 | e, okE := testHamming84Decode(uint8(i)) 49 | if !okE { 50 | assert.False(t, okV) 51 | } else { 52 | assert.True(t, okV) 53 | assert.Equal(t, e, v) 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /bits/parity.go: -------------------------------------------------------------------------------- 1 | package astibits 2 | 3 | var parityTab = [256]uint8{ 4 | 0x00, 0x01, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x01, 0x00, 5 | 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 6 | 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 7 | 0x00, 0x01, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x01, 0x00, 8 | 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 9 | 0x00, 0x01, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x01, 0x00, 10 | 0x00, 0x01, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x01, 0x00, 11 | 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 12 | 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 13 | 0x00, 0x01, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x01, 0x00, 14 | 0x00, 0x01, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x01, 0x00, 15 | 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 16 | 0x00, 0x01, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x01, 0x00, 17 | 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 18 | 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 19 | 0x00, 0x01, 0x01, 0x00, 0x01, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01, 0x01, 0x00, 20 | } 21 | 22 | func Parity(i uint8) (o uint8, ok bool) { 23 | ok = parityTab[i] == 1 24 | o = i & 0x7f 25 | return 26 | } 27 | -------------------------------------------------------------------------------- /bits/parity_test.go: -------------------------------------------------------------------------------- 1 | package astibits 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func testParity(i uint8) bool { 10 | return (i&0x1)^(i>>1&0x1)^(i>>2&0x1)^(i>>3&0x1)^(i>>4&0x1)^(i>>5&0x1)^(i>>6&0x1)^(i>>7&0x1) > 0 11 | } 12 | 13 | func TestParity(t *testing.T) { 14 | for i := 0; i < 256; i++ { 15 | v, okV := Parity(uint8(i)) 16 | okE := testParity(uint8(i)) 17 | if !okE { 18 | assert.False(t, okV) 19 | } else { 20 | assert.True(t, okV) 21 | assert.Equal(t, uint8(i)&0x7f, v) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /byte/iterator.go: -------------------------------------------------------------------------------- 1 | package astibyte 2 | 3 | import "fmt" 4 | 5 | // Iterator represents an object capable of iterating sequentially and safely through a slice of bytes 6 | type Iterator struct { 7 | bs []byte 8 | offset int 9 | } 10 | 11 | // NewIterator creates a new iterator 12 | func NewIterator(bs []byte) *Iterator { 13 | return &Iterator{bs: bs} 14 | } 15 | 16 | // NextByte returns the slice's next byte 17 | func (i *Iterator) NextByte() (b byte, err error) { 18 | if len(i.bs) < i.offset+1 { 19 | err = fmt.Errorf("astits: slice length is %d, offset %d is invalid", len(i.bs), i.offset) 20 | return 21 | } 22 | b = i.bs[i.offset] 23 | i.offset++ 24 | return 25 | } 26 | 27 | // NextBytes returns the n slice's next bytes 28 | func (i *Iterator) NextBytes(n int) (bs []byte, err error) { 29 | if len(i.bs) < i.offset+n { 30 | err = fmt.Errorf("astits: slice length is %d, offset %d is invalid", len(i.bs), i.offset+n) 31 | return 32 | } 33 | bs = make([]byte, n) 34 | copy(bs, i.bs[i.offset:i.offset+n]) 35 | i.offset += n 36 | return 37 | } 38 | 39 | // Seek sets the iterator's offset 40 | func (i *Iterator) Seek(offset int) { 41 | i.offset = offset 42 | } 43 | 44 | // FastForward increments the iterator's offset 45 | func (i *Iterator) FastForward(delta int) { 46 | i.offset += delta 47 | } 48 | 49 | // HasBytesLeft checks whether the slice has some bytes left 50 | func (i *Iterator) HasBytesLeft() bool { 51 | return i.offset < len(i.bs) 52 | } 53 | 54 | // Offset returns the offset 55 | func (i *Iterator) Offset() int { 56 | return i.offset 57 | } 58 | 59 | // Dump dumps the rest of the slice 60 | func (i *Iterator) Dump() (bs []byte) { 61 | if !i.HasBytesLeft() { 62 | return 63 | } 64 | bs = make([]byte, len(i.bs) - i.offset) 65 | copy(bs, i.bs[i.offset:len(i.bs)]) 66 | i.offset = len(i.bs) 67 | return 68 | } 69 | 70 | // Len returns the slice length 71 | func (i *Iterator) Len() int { 72 | return len(i.bs) 73 | } 74 | -------------------------------------------------------------------------------- /byte/iterator_test.go: -------------------------------------------------------------------------------- 1 | package astibyte 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestIterator(t *testing.T) { 10 | // Setup 11 | i := NewIterator([]byte("1234567")) 12 | 13 | // Length 14 | assert.Equal(t, 7, i.Len()) 15 | 16 | // Next byte 17 | b, err := i.NextByte() 18 | assert.NoError(t, err) 19 | assert.Equal(t, byte('1'), b) 20 | 21 | // Next bytes 22 | bs, err := i.NextBytes(3) 23 | assert.NoError(t, err) 24 | assert.Equal(t, []byte("234"), bs) 25 | assert.Equal(t, 4, i.Offset()) 26 | 27 | // Fast forward 28 | i.FastForward(2) 29 | assert.Equal(t, 6, i.Offset()) 30 | assert.True(t, i.HasBytesLeft()) 31 | 32 | // Last byte 33 | b, err = i.NextByte() 34 | assert.NoError(t, err) 35 | assert.Equal(t, byte('7'), b) 36 | assert.False(t, i.HasBytesLeft()) 37 | 38 | // No bytes 39 | b, err = i.NextByte() 40 | assert.Error(t, err) 41 | 42 | // Dump 43 | i.FastForward(-2) 44 | assert.Equal(t, []byte("67"), i.Dump()) 45 | 46 | // Seek 47 | i.Seek(2) 48 | b, err = i.NextByte() 49 | assert.NoError(t, err) 50 | assert.Equal(t, byte('3'), b) 51 | assert.True(t, i.HasBytesLeft()) 52 | } 53 | -------------------------------------------------------------------------------- /byte/length.go: -------------------------------------------------------------------------------- 1 | package astibyte 2 | 3 | // ToLength forces the length of a []byte 4 | func ToLength(i []byte, rpl byte, length int) []byte { 5 | if len(i) == length { 6 | return i 7 | } else if len(i) > length { 8 | return i[:length] 9 | } else { 10 | var o = make([]byte, length) 11 | o = i 12 | for idx := 0; idx < length-len(i); idx++ { 13 | o = append(o, rpl) 14 | } 15 | return o 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /byte/length_test.go: -------------------------------------------------------------------------------- 1 | package astibyte_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/asticode/go-astitools/byte" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestToLength(t *testing.T) { 11 | assert.Equal(t, []byte("test"), astibyte.ToLength([]byte("test"), ' ', 4)) 12 | assert.Equal(t, []byte("test"), astibyte.ToLength([]byte("testtest"), ' ', 4)) 13 | assert.Equal(t, []byte("test "), astibyte.ToLength([]byte("test"), ' ', 6)) 14 | assert.Equal(t, []byte(" "), astibyte.ToLength([]byte{}, ' ', 6)) 15 | } 16 | -------------------------------------------------------------------------------- /byte/pad.go: -------------------------------------------------------------------------------- 1 | package astibyte 2 | 3 | // PadLeft adds n rpl to the left of i so that len is length 4 | func PadLeft(i []byte, rpl byte, length int) []byte { 5 | if len(i) >= length { 6 | return i 7 | } else { 8 | var o = make([]byte, length) 9 | o = i 10 | for idx := 0; idx < length-len(i); idx++ { 11 | o = append([]byte{rpl}, o...) 12 | } 13 | return o 14 | } 15 | } 16 | 17 | // PadRight adds n rpl to the right of i so that len is length 18 | func PadRight(i []byte, rpl byte, length int) []byte { 19 | if len(i) >= length { 20 | return i 21 | } else { 22 | var o = make([]byte, length) 23 | o = i 24 | for idx := 0; idx < length-len(i); idx++ { 25 | o = append(o, rpl) 26 | } 27 | return o 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /byte/pad_test.go: -------------------------------------------------------------------------------- 1 | package astibyte_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/asticode/go-astitools/byte" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestPadLeft(t *testing.T) { 11 | assert.Equal(t, []byte("test"), astibyte.PadLeft([]byte("test"), ' ', 2)) 12 | assert.Equal(t, []byte("test"), astibyte.PadLeft([]byte("test"), ' ', 4)) 13 | assert.Equal(t, []byte(" test"), astibyte.PadLeft([]byte("test"), ' ', 6)) 14 | } 15 | 16 | func TestPadRight(t *testing.T) { 17 | assert.Equal(t, []byte("test"), astibyte.PadRight([]byte("test"), ' ', 2)) 18 | assert.Equal(t, []byte("test"), astibyte.PadRight([]byte("test"), ' ', 4)) 19 | assert.Equal(t, []byte("test "), astibyte.PadRight([]byte("test"), ' ', 6)) 20 | } 21 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package asticonfig 2 | 3 | import ( 4 | "github.com/BurntSushi/toml" 5 | "github.com/imdario/mergo" 6 | "github.com/pkg/errors" 7 | ) 8 | 9 | // New builds a new configuration based on a ptr to the global configuration, the path to the optional toml local 10 | // configuration and a ptr to the flag configuration 11 | func New(global interface{}, localPath string, flag interface{}) (_ interface{}, err error) { 12 | // Local config 13 | if localPath != "" { 14 | if _, err = toml.DecodeFile(localPath, global); err != nil { 15 | err = errors.Wrapf(err, "asticonfig: toml decoding %s failed", localPath) 16 | return 17 | } 18 | } 19 | 20 | // Merge configs 21 | if err = mergo.Merge(flag, global); err != nil { 22 | err = errors.Wrap(err, "asticonfig: merging configs failed") 23 | return 24 | } 25 | return flag, nil 26 | } 27 | -------------------------------------------------------------------------------- /context/canceller.go: -------------------------------------------------------------------------------- 1 | package asticontext 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | ) 7 | 8 | // Canceller represents a context with a mutex 9 | type Canceller struct { 10 | ctx context.Context 11 | cancel context.CancelFunc 12 | mutex *sync.RWMutex 13 | } 14 | 15 | // NewCanceller returns a new canceller 16 | func NewCanceller() (c *Canceller) { 17 | c = &Canceller{mutex: &sync.RWMutex{}} 18 | c.Reset() 19 | return 20 | } 21 | 22 | // Cancel cancels the canceller context 23 | func (c *Canceller) Cancel() { 24 | c.mutex.Lock() 25 | defer c.mutex.Unlock() 26 | c.cancel() 27 | } 28 | 29 | // Cancelled returns whether the canceller has cancelled the context 30 | func (c *Canceller) Cancelled() bool { 31 | return c.ctx.Err() != nil 32 | } 33 | 34 | // Lock locks the canceller mutex 35 | func (c *Canceller) Lock() { 36 | c.mutex.Lock() 37 | } 38 | 39 | // Lock locks the canceller mutex 40 | func (c *Canceller) NewContext() (context.Context, context.CancelFunc) { 41 | return context.WithCancel(c.ctx) 42 | } 43 | 44 | // Reset resets the canceller context 45 | func (c *Canceller) Reset() { 46 | c.ctx, c.cancel = context.WithCancel(context.Background()) 47 | } 48 | 49 | // Unlock unlocks the canceller mutex 50 | func (c *Canceller) Unlock() { 51 | c.mutex.Unlock() 52 | } 53 | -------------------------------------------------------------------------------- /context/canceller_test.go: -------------------------------------------------------------------------------- 1 | package asticontext_test 2 | 3 | import ( 4 | "sync" 5 | "testing" 6 | 7 | "github.com/asticode/go-astitools/context" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestCanceller_Cancel(t *testing.T) { 12 | var c = asticontext.NewCanceller() 13 | var ctx1, cancel1 = c.NewContext() 14 | var ctx2, cancel2 = c.NewContext() 15 | defer cancel1() 16 | defer cancel2() 17 | var wg = &sync.WaitGroup{} 18 | wg.Add(2) 19 | var count int 20 | go func() { 21 | for { 22 | select { 23 | case <-ctx1.Done(): 24 | count += 1 25 | wg.Done() 26 | return 27 | } 28 | } 29 | }() 30 | go func() { 31 | for { 32 | select { 33 | case <-ctx2.Done(): 34 | count += 2 35 | wg.Done() 36 | return 37 | } 38 | } 39 | }() 40 | c.Cancel() 41 | wg.Wait() 42 | assert.Equal(t, 3, count) 43 | assert.True(t, c.Cancelled()) 44 | c.Reset() 45 | assert.False(t, c.Cancelled()) 46 | } 47 | -------------------------------------------------------------------------------- /debug/stack.go: -------------------------------------------------------------------------------- 1 | package astidebug 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "regexp" 7 | "runtime/debug" 8 | "strconv" 9 | ) 10 | 11 | // Vars 12 | var ( 13 | byteLineDelimiter = []byte("\n") 14 | regexpFile = regexp.MustCompile("(.+)\\:(\\d+) \\+(.+)$") 15 | regexpFunction = regexp.MustCompile("(.+)\\(.*\\)$") 16 | ) 17 | 18 | // DebugStack allows testing functions using it 19 | var DebugStack = func() []byte { 20 | return debug.Stack() 21 | } 22 | 23 | // Stack represents a stack 24 | type Stack []StackItem 25 | 26 | // StackItem represents a stack item 27 | type StackItem struct { 28 | Filename string 29 | Function string 30 | Line int 31 | } 32 | 33 | // NewStack returns a new stack 34 | func NewStack() (o Stack) { 35 | var i = &StackItem{} 36 | for _, line := range bytes.Split(DebugStack(), byteLineDelimiter) { 37 | // Trim line 38 | line = bytes.TrimSpace(line) 39 | 40 | // Check line type 41 | var r [][]string 42 | if r = regexpFunction.FindAllStringSubmatch(string(line), -1); len(r) > 0 && len(r[0]) > 1 { 43 | i.Function = r[0][1] 44 | } else if r = regexpFile.FindAllStringSubmatch(string(line), -1); len(r) > 0 && len(r[0]) > 2 { 45 | i.Filename = r[0][1] 46 | i.Line, _ = strconv.Atoi(r[0][2]) 47 | o = append(o, *i) 48 | i = &StackItem{} 49 | } 50 | } 51 | return 52 | } 53 | 54 | // String allows StackItem to implement the Stringer interface 55 | func (i StackItem) String() string { 56 | return fmt.Sprintf("function %s at %s:%d", i.Function, i.Filename, i.Line) 57 | } 58 | -------------------------------------------------------------------------------- /debug/stack_test.go: -------------------------------------------------------------------------------- 1 | package astidebug_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/asticode/go-astitools/debug" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func mockStack() []byte { 11 | return []byte(`goroutine 1 [running]: 12 | runtime/astidebug.Stack(0xc42012e280, 0xc4200f8e17, 0xc4200f8d78) 13 | /usr/local/go/src/runtime/debug/stack.go:24 +0x79 14 | github.com/asticode/myrepo.glob..func2(0x0, 0x0, 0xc4200f8da8) 15 | /home/asticode/projects/go/src/github.com/asticode/myrepo/sync.go:55 +0x22 16 | github.com/asticode/myrepo.(*MyStruct).LogMessage(0xc420105e00, 0x7a2108, 0x11, 0x7d0710, 0xc4200f8e60) 17 | /home/asticode/projects/go/src/github.com/asticode/myrepo/sync.go:70 +0x3b 18 | github.com/asticode/myrepo.(*MyStruct).RUnlock.func1(0xc42001d980, 0xc4201dc000, 0x14, 0xc420105e00) 19 | /home/asticode/projects/go/src/github.com/asticode/myrepo/sync.go:147 +0x70 20 | github.com/asticode/myrepo.(*MyStruct).RUnlock(0xc420105e00, 0xc42001d980, 0xc4201dc000, 0x14) 21 | /home/asticode/projects/go/src/github.com/asticode/myrepo/sync.go:151 +0x3d 22 | main.(*Worker).Retire(0xc4200f1800, 0x90cfa0, 0xc420018d70) 23 | /home/asticode/projects/go/src/github.com/asticode/myproject/worker.go:174 +0x11d 24 | main.main() 25 | /home/asticode/projects/go/src/github.com/asticode/myproject/main.go:76 +0x571`) 26 | } 27 | 28 | func TestNewStack(t *testing.T) { 29 | astidebug.DebugStack = func() []byte { 30 | return mockStack() 31 | } 32 | var s = astidebug.NewStack() 33 | assert.Equal(t, astidebug.Stack{astidebug.StackItem{Filename: "/usr/local/go/src/runtime/debug/stack.go", Function: "runtime/astidebug.Stack", Line: 24}, astidebug.StackItem{Filename: "/home/asticode/projects/go/src/github.com/asticode/myrepo/sync.go", Function: "github.com/asticode/myrepo.glob..func2", Line: 55}, astidebug.StackItem{Filename: "/home/asticode/projects/go/src/github.com/asticode/myrepo/sync.go", Function: "github.com/asticode/myrepo.(*MyStruct).LogMessage", Line: 70}, astidebug.StackItem{Filename: "/home/asticode/projects/go/src/github.com/asticode/myrepo/sync.go", Function: "github.com/asticode/myrepo.(*MyStruct).RUnlock.func1", Line: 147}, astidebug.StackItem{Filename: "/home/asticode/projects/go/src/github.com/asticode/myrepo/sync.go", Function: "github.com/asticode/myrepo.(*MyStruct).RUnlock", Line: 151}, astidebug.StackItem{Filename: "/home/asticode/projects/go/src/github.com/asticode/myproject/worker.go", Function: "main.(*Worker).Retire", Line: 174}, astidebug.StackItem{Filename: "/home/asticode/projects/go/src/github.com/asticode/myproject/main.go", Function: "main.main", Line: 76}}, s) 34 | } 35 | -------------------------------------------------------------------------------- /defer/closer.go: -------------------------------------------------------------------------------- 1 | package astidefer 2 | 3 | import ( 4 | "sync" 5 | 6 | astierror "github.com/asticode/go-astitools/error" 7 | ) 8 | 9 | // Closer is an object that can close things 10 | type Closer struct { 11 | fs []CloseFunc 12 | m *sync.Mutex 13 | o *sync.Once 14 | } 15 | 16 | // CloseFunc is a method that closes something 17 | type CloseFunc func() error 18 | 19 | // NewCloser creates a new closer 20 | func NewCloser() *Closer { 21 | return &Closer{ 22 | m: &sync.Mutex{}, 23 | o: &sync.Once{}, 24 | } 25 | } 26 | 27 | // Close implements the io.Closer interface 28 | func (c *Closer) Close() (err error) { 29 | c.o.Do(func() { 30 | // Get close funcs 31 | c.m.Lock() 32 | fs := append([]CloseFunc{}, c.fs...) 33 | c.m.Unlock() 34 | 35 | // Loop through closers 36 | var errs []error 37 | for _, f := range fs { 38 | if errC := f(); errC != nil { 39 | errs = append(errs, errC) 40 | } 41 | } 42 | 43 | // Process errors 44 | if len(errs) == 1 { 45 | err = errs[0] 46 | } else if len(errs) > 1 { 47 | err = astierror.NewMultiple(errs) 48 | } 49 | }) 50 | return 51 | } 52 | 53 | // Add adds a close func at the beginning of the list 54 | func (c *Closer) Add(f CloseFunc) { 55 | c.m.Lock() 56 | defer c.m.Unlock() 57 | c.fs = append([]CloseFunc{f}, c.fs...) 58 | } 59 | 60 | // NewChild creates a new child closer 61 | func (c *Closer) NewChild() (child *Closer) { 62 | child = NewCloser() 63 | c.Add(child.Close) 64 | return 65 | } 66 | -------------------------------------------------------------------------------- /defer/closer_test.go: -------------------------------------------------------------------------------- 1 | package astidefer 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/pkg/errors" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestCloser(t *testing.T) { 11 | c := NewCloser() 12 | var o []string 13 | c.Add(func() error { 14 | o = append(o, "1") 15 | return nil 16 | }) 17 | c.Add(func() error { 18 | o = append(o, "2") 19 | return errors.New("1") 20 | }) 21 | c.Add(func() error { return errors.New("2") }) 22 | err := c.Close() 23 | assert.Equal(t, []string{"2", "1"}, o) 24 | assert.Equal(t, "2 | 1", err.Error()) 25 | } 26 | -------------------------------------------------------------------------------- /error/multiple.go: -------------------------------------------------------------------------------- 1 | package astierror 2 | 3 | import "strings" 4 | 5 | // Multiple is an object containing multiple errors 6 | type Multiple []error 7 | 8 | // NewMultiple creates new multiple errors 9 | func NewMultiple(errs []error) Multiple { 10 | return Multiple(errs) 11 | } 12 | 13 | // Error implements the error interface 14 | func (m Multiple) Error() string { 15 | var ss []string 16 | for _, err := range m { 17 | ss = append(ss, err.Error()) 18 | } 19 | return strings.Join(ss, " | ") 20 | } 21 | -------------------------------------------------------------------------------- /event/dispatcher.go: -------------------------------------------------------------------------------- 1 | package astievent 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | 7 | astisync "github.com/asticode/go-astitools/sync" 8 | ) 9 | 10 | // Dispatcher represents an object that can dispatch simple events (name + payload) 11 | type Dispatcher struct { 12 | c *astisync.Chan 13 | hs map[string][]EventHandler 14 | mh *sync.Mutex 15 | } 16 | 17 | // EventHandler represents a function that can handler an event's payload 18 | type EventHandler func(payload interface{}) 19 | 20 | // NewDispatcher creates a new dispatcher 21 | func NewDispatcher() *Dispatcher { 22 | return &Dispatcher{ 23 | c: astisync.NewChan(astisync.ChanOptions{}), 24 | hs: make(map[string][]EventHandler), 25 | mh: &sync.Mutex{}, 26 | } 27 | } 28 | 29 | // On adds an event handler for a specific name 30 | func (d *Dispatcher) On(name string, h EventHandler) { 31 | // Lock 32 | d.mh.Lock() 33 | defer d.mh.Unlock() 34 | 35 | // Add handler 36 | d.hs[name] = append(d.hs[name], h) 37 | } 38 | 39 | // Dispatch dispatches a payload for a specific name 40 | func (d *Dispatcher) Dispatch(name string, payload interface{}) { 41 | // Lock 42 | d.mh.Lock() 43 | defer d.mh.Unlock() 44 | 45 | // No handlers 46 | hs, ok := d.hs[name] 47 | if !ok { 48 | return 49 | } 50 | 51 | // Loop through handlers 52 | for _, h := range hs { 53 | // We need to store the handler 54 | sh := h 55 | 56 | // Add to chan 57 | d.c.Add(func() { 58 | sh(payload) 59 | }) 60 | } 61 | } 62 | 63 | // Start starts the dispatcher. It is blocking 64 | func (d *Dispatcher) Start(ctx context.Context) { 65 | d.c.Start(ctx) 66 | } 67 | 68 | // Stop stops the dispatcher 69 | func (d *Dispatcher) Stop() { 70 | d.c.Stop() 71 | } 72 | 73 | // Reset resets the dispatcher 74 | func (d *Dispatcher) Reset() { 75 | d.c.Reset() 76 | } 77 | -------------------------------------------------------------------------------- /exec/browser.go: -------------------------------------------------------------------------------- 1 | package astiexec 2 | 3 | import ( 4 | "context" 5 | "os/exec" 6 | "runtime" 7 | 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | // Cheers to https://gist.github.com/threeaccents/607f3bc3a57a2ddd9d57 12 | func OpenBrowser(ctx context.Context, url string) error { 13 | var args []string 14 | switch runtime.GOOS { 15 | case "darwin": 16 | args = []string{"open"} 17 | case "windows": 18 | args = []string{"cmd", "/c", "start"} 19 | default: 20 | args = []string{"xdg-open"} 21 | } 22 | cmd := exec.CommandContext(ctx, args[0], append(args[1:], url)...) 23 | if b, err := cmd.CombinedOutput(); err != nil { 24 | return errors.Wrapf(err, "astiexec: opening browser failed with body %s", b) 25 | } 26 | return nil 27 | } 28 | -------------------------------------------------------------------------------- /exec/cmd.go: -------------------------------------------------------------------------------- 1 | package astiexec 2 | 3 | import ( 4 | "context" 5 | "os/exec" 6 | "strings" 7 | "time" 8 | 9 | "github.com/asticode/go-astilog" 10 | ) 11 | 12 | // Cmd represents a command 13 | type Cmd struct { 14 | Args []string 15 | ctx context.Context 16 | } 17 | 18 | // NewCmd creates a new command 19 | func NewCmd(ctx context.Context, args ...string) (cmd *Cmd) { 20 | cmd = &Cmd{ 21 | Args: args, 22 | ctx: ctx, 23 | } 24 | return 25 | } 26 | 27 | // String allows Cmd to implements the stringify interface 28 | func (c *Cmd) String() string { 29 | return strings.Join(c.Args, " ") 30 | } 31 | 32 | // Exec executes a command 33 | var Exec = func(cmd *Cmd) (o []byte, d time.Duration, err error) { 34 | // Init 35 | defer func(t time.Time) { 36 | d = time.Since(t) 37 | }(time.Now()) 38 | 39 | // Create exec command 40 | execCmd := exec.CommandContext(cmd.ctx, cmd.Args[0], cmd.Args[1:]...) 41 | 42 | // Execute command 43 | astilog.Debugf("Executing %s", cmd) 44 | o, err = execCmd.CombinedOutput() 45 | return 46 | } 47 | -------------------------------------------------------------------------------- /exec/cmd_test.go: -------------------------------------------------------------------------------- 1 | package astiexec_test 2 | 3 | import ( 4 | "sync" 5 | "testing" 6 | "time" 7 | 8 | "github.com/asticode/go-astitools/exec" 9 | "github.com/stretchr/testify/assert" 10 | "golang.org/x/net/context" 11 | ) 12 | 13 | func TestWithTimeout(t *testing.T) { 14 | // Success 15 | var ctx, _ = context.WithTimeout(context.Background(), time.Second) 16 | var cmd = astiexec.NewCmd(ctx, "sleep", "0.5") 17 | assert.Equal(t, "sleep 0.5", cmd.String()) 18 | _, _, err := astiexec.Exec(cmd) 19 | assert.NoError(t, err) 20 | 21 | // Timeout 22 | ctx, _ = context.WithTimeout(context.Background(), time.Millisecond) 23 | cmd = astiexec.NewCmd(ctx, "sleep", "0.5") 24 | _, _, err = astiexec.Exec(cmd) 25 | assert.EqualError(t, err, "signal: killed") 26 | 27 | // Cancel 28 | ctx, cancel := context.WithTimeout(context.Background(), time.Second) 29 | cmd = astiexec.NewCmd(ctx, "sleep", "0.5") 30 | var wg = &sync.WaitGroup{} 31 | wg.Add(1) 32 | go func() { 33 | defer wg.Done() 34 | _, _, err = astiexec.Exec(cmd) 35 | }() 36 | cancel() 37 | wg.Wait() 38 | assert.EqualError(t, err, "context canceled") 39 | } 40 | -------------------------------------------------------------------------------- /exec/writer.go: -------------------------------------------------------------------------------- 1 | package astiexec 2 | 3 | import "bytes" 4 | 5 | // Vars 6 | var ( 7 | bytesEOL = []byte("\n") 8 | ) 9 | 10 | // StdWriter represents an object capable of writing what's coming out of stdout or stderr 11 | type StdWriter struct { 12 | buffer *bytes.Buffer 13 | fn func(i []byte) 14 | } 15 | 16 | // NewStdWriter creates a new StdWriter 17 | func NewStdWriter(fn func(i []byte)) *StdWriter { 18 | return &StdWriter{buffer: &bytes.Buffer{}, fn: fn} 19 | } 20 | 21 | // Close closes the writer 22 | func (w *StdWriter) Close() { 23 | if w.buffer.Len() > 0 { 24 | w.write(w.buffer.Bytes()) 25 | } 26 | } 27 | 28 | // Write implements the io.Writer interface 29 | func (w *StdWriter) Write(i []byte) (n int, err error) { 30 | // Update n to avoid broken pipe error 31 | defer func() { 32 | n = len(i) 33 | }() 34 | 35 | // No EOL in the log, write in buffer 36 | if bytes.Index(i, bytesEOL) == -1 { 37 | w.buffer.Write(i) 38 | return 39 | } 40 | 41 | // Loop in items split by EOL 42 | var items = bytes.Split(i, bytesEOL) 43 | for i := 0; i < len(items)-1; i++ { 44 | // If first item, add the buffer 45 | if i == 0 { 46 | items[i] = append(w.buffer.Bytes(), items[i]...) 47 | w.buffer.Reset() 48 | } 49 | 50 | // Log 51 | w.write(items[i]) 52 | } 53 | 54 | // Add remaining to buffer 55 | w.buffer.Write(items[len(items)-1]) 56 | return 57 | } 58 | 59 | // write writes the input 60 | func (w *StdWriter) write(i []byte) { 61 | w.fn(i) 62 | } 63 | -------------------------------------------------------------------------------- /exec/writer_test.go: -------------------------------------------------------------------------------- 1 | package astiexec_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/asticode/go-astitools/exec" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestStdWriter(t *testing.T) { 11 | // Init 12 | var o []string 13 | var w = astiexec.NewStdWriter(func(i []byte) { 14 | o = append(o, string(i)) 15 | }) 16 | 17 | // No EOL 18 | w.Write([]byte("bla bla ")) 19 | assert.Empty(t, o) 20 | 21 | // Multi EOL 22 | w.Write([]byte("bla \nbla bla\nbla")) 23 | assert.Equal(t, []string{"bla bla bla ", "bla bla"}, o) 24 | 25 | // Close 26 | w.Close() 27 | assert.Equal(t, []string{"bla bla bla ", "bla bla", "bla"}, o) 28 | } 29 | -------------------------------------------------------------------------------- /flag/flag.go: -------------------------------------------------------------------------------- 1 | package astiflag 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | ) 7 | 8 | // Subcommand retrieves the subcommand from the input Args 9 | func Subcommand() (o string) { 10 | if len(os.Args) >= 2 && os.Args[1][0] != '-' { 11 | o = os.Args[1] 12 | os.Args = append([]string{os.Args[0]}, os.Args[2:]...) 13 | } 14 | return 15 | } 16 | 17 | // Strings represents a flag that can be set several times that will output a []string 18 | type Strings []string 19 | 20 | // String implements the flag.Value interface 21 | func (f *Strings) String() string { 22 | return strings.Join(*f, ",") 23 | } 24 | 25 | // Set implements the flag.Value interface 26 | func (f *Strings) Set(i string) error { 27 | *f = append(*f, i) 28 | return nil 29 | } 30 | 31 | // StringsMap represents a flag that can be set several times that will output a map[string]bool 32 | type StringsMap map[string]bool 33 | 34 | // NewStringsMap creates a new StringsMap 35 | func NewStringsMap() StringsMap { 36 | return StringsMap(make(map[string]bool)) 37 | } 38 | 39 | // String implements the flag.Value interface 40 | func (f StringsMap) String() string { 41 | var s []string 42 | for k := range f { 43 | s = append(s, k) 44 | } 45 | return strings.Join(s, ",") 46 | } 47 | 48 | // Set implements the flag.Value interface 49 | func (f StringsMap) Set(i string) error { 50 | f[i] = true 51 | return nil 52 | } 53 | -------------------------------------------------------------------------------- /flag/flag_test.go: -------------------------------------------------------------------------------- 1 | package astiflag_test 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/asticode/go-astitools/flag" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestSubcommand(t *testing.T) { 12 | os.Args = []string{"bite"} 13 | assert.Equal(t, "", astiflag.Subcommand()) 14 | os.Args = []string{"bite", "-caca"} 15 | assert.Equal(t, "", astiflag.Subcommand()) 16 | os.Args = []string{"bite", "caca"} 17 | assert.Equal(t, "caca", astiflag.Subcommand()) 18 | } 19 | -------------------------------------------------------------------------------- /flag/time.go: -------------------------------------------------------------------------------- 1 | package astiflag 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // Time represents a time flag 8 | type Time struct { 9 | time.Time 10 | } 11 | 12 | // Set implements the flag.Value interface 13 | func (f *Time) Set(i string) (err error) { 14 | f.Time, err = time.Parse(time.RFC3339, i) 15 | return 16 | } 17 | -------------------------------------------------------------------------------- /flag/time_test.go: -------------------------------------------------------------------------------- 1 | package astiflag_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/asticode/go-astitools/flag" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestTime(t *testing.T) { 12 | const tm = "2017-12-13T12:34:56+02:00" 13 | ft := &astiflag.Time{} 14 | err := ft.Set(tm) 15 | assert.NoError(t, err) 16 | at, _ := time.Parse(time.RFC3339, tm) 17 | assert.Equal(t, at, ft.Time) 18 | } 19 | -------------------------------------------------------------------------------- /float/rational.go: -------------------------------------------------------------------------------- 1 | package astifloat 2 | 3 | import ( 4 | "bytes" 5 | "strconv" 6 | 7 | "fmt" 8 | 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | // Rational represents a rational 13 | type Rational struct{ den, num int } 14 | 15 | // NewRational creates a new rational 16 | func NewRational(num, den int) *Rational { 17 | return &Rational{ 18 | den: den, 19 | num: num, 20 | } 21 | } 22 | 23 | // Num returns the rational num 24 | func (r *Rational) Num() int { 25 | return r.num 26 | } 27 | 28 | // Den returns the rational den 29 | func (r *Rational) Den() int { 30 | return r.den 31 | } 32 | 33 | // ToFloat64 returns the rational as a float64 34 | func (r *Rational) ToFloat64() float64 { 35 | return float64(r.num) / float64(r.den) 36 | } 37 | 38 | // MarshalText implements the TextMarshaler interface 39 | func (r *Rational) MarshalText() (b []byte, err error) { 40 | b = []byte(fmt.Sprintf("%d/%d", r.num, r.den)) 41 | return 42 | } 43 | 44 | // UnmarshalText implements the TextUnmarshaler interface 45 | func (r *Rational) UnmarshalText(b []byte) (err error) { 46 | r.num = 0 47 | r.den = 1 48 | if len(b) == 0 { 49 | return 50 | } 51 | items := bytes.Split(b, []byte("/")) 52 | if r.num, err = strconv.Atoi(string(items[0])); err != nil { 53 | err = errors.Wrapf(err, "astifloat: atoi of %s failed", string(items[0])) 54 | return 55 | } 56 | if len(items) > 1 { 57 | if r.den, err = strconv.Atoi(string(items[1])); err != nil { 58 | err = errors.Wrapf(err, "astifloat: atoi of %s failed", string(items[1])) 59 | return 60 | } 61 | } 62 | return 63 | } 64 | -------------------------------------------------------------------------------- /float/rational_test.go: -------------------------------------------------------------------------------- 1 | package astifloat 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestRational(t *testing.T) { 10 | r := &Rational{} 11 | err := r.UnmarshalText([]byte("")) 12 | assert.NoError(t, err) 13 | assert.Equal(t, 0.0, r.ToFloat64()) 14 | err = r.UnmarshalText([]byte("test")) 15 | assert.Error(t, err) 16 | err = r.UnmarshalText([]byte("1/test")) 17 | assert.Error(t, err) 18 | err = r.UnmarshalText([]byte("0")) 19 | assert.NoError(t, err) 20 | assert.Equal(t, 0, r.Num()) 21 | assert.Equal(t, 1, r.Den()) 22 | err = r.UnmarshalText([]byte("1/2")) 23 | assert.NoError(t, err) 24 | assert.Equal(t, 1, r.Num()) 25 | assert.Equal(t, 2, r.Den()) 26 | assert.Equal(t, 0.5, r.ToFloat64()) 27 | } 28 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/asticode/go-astitools 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/BurntSushi/toml v0.3.1 7 | github.com/asticode/go-astiamqp v1.2.0 8 | github.com/asticode/go-astilog v1.3.0 9 | github.com/asticode/go-astiws v1.0.1 10 | github.com/imdario/mergo v0.3.8 11 | github.com/julienschmidt/httprouter v1.3.0 12 | github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect 13 | github.com/mattn/go-isatty v0.0.10 // indirect 14 | github.com/pkg/errors v0.8.1 15 | github.com/stretchr/testify v1.4.0 16 | golang.org/x/crypto v0.0.0-20191205161847-0a08dada0ff9 17 | golang.org/x/net v0.0.0-20191204025024-5ee1b9f4859a 18 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e // indirect 19 | golang.org/x/text v0.3.2 20 | ) 21 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/asticode/go-astiamqp v1.2.0 h1:OxHb9V0PY5XYOp343BgtPHXaBg8gIp02X7+f8Vyg/BI= 4 | github.com/asticode/go-astiamqp v1.2.0/go.mod h1:bnOKJD4/B2T87fLEjnYFwEK1T4X/5iX/Nv9+NeJZa/s= 5 | github.com/asticode/go-astikit v0.0.2 h1:xe70qRPxfKWvA/XhYKY9/O5Dk3bCIAYnQG9cbcrA37I= 6 | github.com/asticode/go-astikit v0.0.2/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0= 7 | github.com/asticode/go-astilog v1.2.0 h1:p1jTI/4CgUgtkuAVP6DiRn6RTjX6CIqLdn9pu6tsz5w= 8 | github.com/asticode/go-astilog v1.2.0/go.mod h1:0sGDwdYLRSFVbbvrXLbJC9niyGWL5JGTCEQWFJKHZr0= 9 | github.com/asticode/go-astilog v1.3.0 h1:5QwZPQDLkLPE3JzibEJwOPEEWT37anOQAy5MQ7XkvCQ= 10 | github.com/asticode/go-astilog v1.3.0/go.mod h1:0sGDwdYLRSFVbbvrXLbJC9niyGWL5JGTCEQWFJKHZr0= 11 | github.com/asticode/go-astiws v1.0.1 h1:eySPGaUjECeu4/bqdFM+YM0khv4Mfd69bYUtDeD7PRU= 12 | github.com/asticode/go-astiws v1.0.1/go.mod h1:5F/0RkJTq1SZ0TsiheD3j/9lr9kla70c2q7w1aAG78Q= 13 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 15 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 16 | github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= 17 | github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 18 | github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ= 19 | github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= 20 | github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= 21 | github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 22 | github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= 23 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 24 | github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= 25 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 26 | github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= 27 | github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 28 | github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= 29 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 30 | github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10= 31 | github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= 32 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 33 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 34 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 35 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 36 | github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= 37 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 38 | github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271 h1:WhxRHzgeVGETMlmVfqhRn8RIeeNoPr2Czh33I4Zdccw= 39 | github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= 40 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 41 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 42 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 43 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 44 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 45 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 46 | golang.org/x/crypto v0.0.0-20191202143827-86a70503ff7e h1:egKlR8l7Nu9vHGWbcUV8lqR4987UfUbBd7GbhqGzNYU= 47 | golang.org/x/crypto v0.0.0-20191202143827-86a70503ff7e/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 48 | golang.org/x/crypto v0.0.0-20191205161847-0a08dada0ff9 h1:abxekknhS/Drh3uoQDk5Hc7BgeiyI39Crb7vhf/1j5s= 49 | golang.org/x/crypto v0.0.0-20191205161847-0a08dada0ff9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 50 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 51 | golang.org/x/net v0.0.0-20191204025024-5ee1b9f4859a h1:+HHJiFUXVOIS9mr1ThqkQD1N8vpFCfCShqADBM12KTc= 52 | golang.org/x/net v0.0.0-20191204025024-5ee1b9f4859a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 53 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 54 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 55 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 56 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= 57 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 58 | golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 59 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e h1:9vRrk9YW2BTzLP0VCB9ZDjU4cPqkg+IDWL7XgxA1yxQ= 60 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 61 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 62 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 63 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 64 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 65 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 66 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 67 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 68 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 69 | -------------------------------------------------------------------------------- /http/downloader.go: -------------------------------------------------------------------------------- 1 | package astihttp 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "os" 10 | "path/filepath" 11 | "sync" 12 | 13 | "github.com/asticode/go-astilog" 14 | "github.com/asticode/go-astitools/io" 15 | "github.com/pkg/errors" 16 | ) 17 | 18 | // Downloader represents a downloader 19 | type Downloader struct { 20 | bp *sync.Pool 21 | busyWorkers int 22 | cond *sync.Cond 23 | ignoreErrors bool 24 | mc *sync.Mutex // Locks cond 25 | mw *sync.Mutex // Locks busyWorkers 26 | numberOfWorkers int 27 | s *Sender 28 | } 29 | 30 | // DownloaderFunc represents a downloader func 31 | // It's its responsibility to close the reader 32 | type DownloaderFunc func(ctx context.Context, idx int, src string, r io.ReadCloser) error 33 | 34 | // DownloaderOptions represents downloader options 35 | type DownloaderOptions struct { 36 | IgnoreErrors bool 37 | NumberOfWorkers int 38 | Sender SenderOptions 39 | } 40 | 41 | // NewDownloader creates a new downloader 42 | func NewDownloader(o DownloaderOptions) (d *Downloader) { 43 | d = &Downloader{ 44 | bp: &sync.Pool{New: func() interface{} { return &bytes.Buffer{} }}, 45 | ignoreErrors: o.IgnoreErrors, 46 | mc: &sync.Mutex{}, 47 | mw: &sync.Mutex{}, 48 | numberOfWorkers: o.NumberOfWorkers, 49 | s: NewSender(o.Sender), 50 | } 51 | d.cond = sync.NewCond(d.mc) 52 | if d.numberOfWorkers == 0 { 53 | d.numberOfWorkers = 1 54 | } 55 | return 56 | } 57 | 58 | // Download downloads in parallel a set of src paths and executes a custom callback on each downloaded buffers 59 | func (d *Downloader) Download(parentCtx context.Context, paths []string, fn DownloaderFunc) (err error) { 60 | // Init 61 | ctx, cancel := context.WithCancel(parentCtx) 62 | m := &sync.Mutex{} // Locks err 63 | gwg := &sync.WaitGroup{} 64 | gwg.Add(len(paths)) 65 | lwg := &sync.WaitGroup{} 66 | 67 | // Loop through src paths 68 | var idx int 69 | for idx < len(paths) { 70 | // Check context 71 | if parentCtx.Err() != nil { 72 | m.Lock() 73 | err = errors.Wrap(err, "astihttp: context error") 74 | m.Unlock() 75 | return 76 | } 77 | 78 | // Lock cond here in case a worker finishes between checking the number of busy workers and the if statement 79 | d.cond.L.Lock() 80 | 81 | // Check if a worker is available 82 | var ok bool 83 | d.mw.Lock() 84 | if ok = d.numberOfWorkers > d.busyWorkers; ok { 85 | d.busyWorkers++ 86 | } 87 | d.mw.Unlock() 88 | 89 | // No worker is available 90 | if !ok { 91 | d.cond.Wait() 92 | d.cond.L.Unlock() 93 | continue 94 | } 95 | d.cond.L.Unlock() 96 | 97 | // Check error 98 | m.Lock() 99 | if err != nil { 100 | m.Unlock() 101 | lwg.Wait() 102 | return 103 | } 104 | m.Unlock() 105 | 106 | // Download 107 | go func(idx int) { 108 | lwg.Add(1) 109 | if errR := d.download(ctx, idx, paths[idx], fn, gwg); errR != nil { 110 | m.Lock() 111 | if err == nil { 112 | err = errR 113 | } 114 | m.Unlock() 115 | cancel() 116 | } 117 | lwg.Done() 118 | }(idx) 119 | idx++ 120 | } 121 | gwg.Wait() 122 | return 123 | } 124 | 125 | type readCloser struct { 126 | b *bytes.Buffer 127 | bp *sync.Pool 128 | } 129 | 130 | func newReadCloser(b *bytes.Buffer, bp *sync.Pool) *readCloser { 131 | return &readCloser{ 132 | b: b, 133 | bp: bp, 134 | } 135 | } 136 | 137 | // Read implements the io.Reader interface 138 | func (c readCloser) Read(p []byte) (n int, err error) { 139 | return c.b.Read(p) 140 | } 141 | 142 | // Close implements the io.Closer interface 143 | func (c readCloser) Close() error { 144 | c.b.Reset() 145 | c.bp.Put(c.b) 146 | return nil 147 | } 148 | 149 | func (d *Downloader) download(ctx context.Context, idx int, path string, fn DownloaderFunc, wg *sync.WaitGroup) (err error) { 150 | // Update wait group and worker status 151 | defer func() { 152 | // Update worker status 153 | d.mw.Lock() 154 | d.busyWorkers-- 155 | d.mw.Unlock() 156 | 157 | // Broadcast 158 | d.cond.L.Lock() 159 | d.cond.Broadcast() 160 | d.cond.L.Unlock() 161 | 162 | // Update wait group 163 | wg.Done() 164 | }() 165 | 166 | // Create request 167 | var r *http.Request 168 | if r, err = http.NewRequest(http.MethodGet, path, nil); err != nil { 169 | return errors.Wrapf(err, "astihttp: creating GET request to %s failed", path) 170 | } 171 | 172 | // Send request 173 | var resp *http.Response 174 | if resp, err = d.s.Send(r); err != nil { 175 | return errors.Wrapf(err, "astihttp: sending GET request to %s failed", path) 176 | } 177 | defer resp.Body.Close() 178 | 179 | // Validate status code 180 | buf := newReadCloser(d.bp.Get().(*bytes.Buffer), d.bp) 181 | if resp.StatusCode != http.StatusOK { 182 | errS := fmt.Errorf("astihttp: sending GET request to %s returned %d status code", path, resp.StatusCode) 183 | if !d.ignoreErrors { 184 | return errS 185 | } else { 186 | astilog.Error(errors.Wrap(errS, "astihttp: ignoring error")) 187 | } 188 | } else { 189 | // Copy body 190 | if _, err = astiio.Copy(ctx, resp.Body, buf.b); err != nil { 191 | return errors.Wrap(err, "astihttp: copying resp.Body to buf.b failed") 192 | } 193 | } 194 | 195 | // Custom callback 196 | if err = fn(ctx, idx, path, buf); err != nil { 197 | return errors.Wrapf(err, "astihttp: custom callback on %s failed", path) 198 | } 199 | return 200 | } 201 | 202 | // DownloadInDirectory downloads in parallel a set of src paths and saves them in a dst directory 203 | func (d *Downloader) DownloadInDirectory(ctx context.Context, dst string, paths ...string) error { 204 | return d.Download(ctx, paths, func(ctx context.Context, idx int, path string, r io.ReadCloser) (err error) { 205 | // Make sure to close the reader 206 | defer r.Close() 207 | 208 | // Make sure destination directory exists 209 | if err = os.MkdirAll(dst, 0700); err != nil { 210 | err = errors.Wrapf(err, "astihttp: mkdirall %s failed", dst) 211 | return 212 | } 213 | 214 | // Create destination file 215 | var f *os.File 216 | dst := filepath.Join(dst, filepath.Base(path)) 217 | if f, err = os.Create(dst); err != nil { 218 | err = errors.Wrapf(err, "astihttp: creating %s failed", dst) 219 | return 220 | } 221 | defer f.Close() 222 | 223 | // Copy 224 | if _, err = astiio.Copy(ctx, r, f); err != nil { 225 | err = errors.Wrapf(err, "astihttp: copying content to %s failed", dst) 226 | return 227 | } 228 | return 229 | }) 230 | } 231 | 232 | type chunk struct { 233 | idx int 234 | r io.ReadCloser 235 | path string 236 | } 237 | 238 | // DownloadInWriter downloads in parallel a set of src paths and concatenates them in order in a writer 239 | func (d *Downloader) DownloadInWriter(ctx context.Context, w io.Writer, paths ...string) (err error) { 240 | // Download 241 | var cs []chunk 242 | var m sync.Mutex // Locks cs 243 | var requiredIdx int 244 | err = d.Download(ctx, paths, func(ctx context.Context, idx int, path string, r io.ReadCloser) (err error) { 245 | // Lock 246 | m.Lock() 247 | defer m.Unlock() 248 | 249 | // Check where to insert chunk 250 | var idxInsert = -1 251 | for idxChunk := 0; idxChunk < len(cs); idxChunk++ { 252 | if idx < cs[idxChunk].idx { 253 | idxInsert = idxChunk 254 | break 255 | } 256 | } 257 | 258 | // Create chunk 259 | c := chunk{ 260 | idx: idx, 261 | path: path, 262 | r: r, 263 | } 264 | 265 | // Add chunk 266 | if idxInsert > -1 { 267 | cs = append(cs[:idxInsert], append([]chunk{c}, cs[idxInsert:]...)...) 268 | } else { 269 | cs = append(cs, c) 270 | } 271 | 272 | // Loop through chunks 273 | for idxChunk := 0; idxChunk < len(cs); idxChunk++ { 274 | // Get chunk 275 | c := cs[idxChunk] 276 | 277 | // The chunk should be copied 278 | if c.idx == requiredIdx { 279 | // Copy chunk content 280 | _, err = astiio.Copy(ctx, c.r, w) 281 | 282 | // Make sure the reader is closed 283 | c.r.Close() 284 | 285 | // Remove chunk 286 | requiredIdx++ 287 | cs = append(cs[:idxChunk], cs[idxChunk+1:]...) 288 | idxChunk-- 289 | 290 | // Check error now so that chunk is still removed and reader is closed 291 | if err != nil { 292 | err = errors.Wrapf(err, "astihttp: copying chunk #%d to dst failed", c.idx) 293 | return 294 | } 295 | } 296 | } 297 | return 298 | }) 299 | 300 | // Make sure to close all readers 301 | for _, c := range cs { 302 | c.r.Close() 303 | } 304 | return 305 | } 306 | 307 | // DownloadInFile downloads in parallel a set of src paths and concatenates them in order in a writer 308 | func (d *Downloader) DownloadInFile(ctx context.Context, dst string, paths ...string) (err error) { 309 | // Make sure destination directory exists 310 | if err = os.MkdirAll(filepath.Dir(dst), 0700); err != nil { 311 | err = errors.Wrapf(err, "astihttp: mkdirall %s failed", filepath.Dir(dst)) 312 | return 313 | } 314 | 315 | // Create destination file 316 | var f *os.File 317 | if f, err = os.Create(dst); err != nil { 318 | err = errors.Wrapf(err, "astihttp: creating %s failed", dst) 319 | return 320 | } 321 | defer f.Close() 322 | 323 | // Download in writer 324 | return d.DownloadInWriter(ctx, f, paths...) 325 | } 326 | -------------------------------------------------------------------------------- /http/downloader_test.go: -------------------------------------------------------------------------------- 1 | package astihttp 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "net/http" 7 | "net/http/httptest" 8 | "sync" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestDownloader(t *testing.T) { 15 | // Init 16 | m := &sync.Mutex{} 17 | m.Lock() 18 | s := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { 19 | switch r.URL.String() { 20 | case "/1": 21 | rw.Write([]byte("1")) 22 | case "/2": 23 | m.Lock() 24 | rw.Write([]byte("2")) 25 | case "/3": 26 | rw.Write([]byte("3")) 27 | m.Unlock() 28 | case "/4": 29 | rw.Write([]byte("4")) 30 | } 31 | })) 32 | defer s.Close() 33 | 34 | // Download in writer 35 | buf := &bytes.Buffer{} 36 | d := NewDownloader(DownloaderOptions{NumberOfWorkers: 2}) 37 | err := d.DownloadInWriter(context.Background(), buf, s.URL+"/1", s.URL+"/2", s.URL+"/3", s.URL+"/4") 38 | assert.NoError(t, err) 39 | assert.Equal(t, "1234", buf.String()) 40 | } 41 | -------------------------------------------------------------------------------- /http/http.go: -------------------------------------------------------------------------------- 1 | package astihttp 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "os" 8 | 9 | "io" 10 | 11 | "github.com/asticode/go-astitools/io" 12 | "github.com/pkg/errors" 13 | ) 14 | 15 | // Download is a cancellable function that downloads a src into a dst using a specific *http.Client 16 | func Download(ctx context.Context, c *http.Client, src, dst string) (err error) { 17 | // Create the dst file 18 | var f *os.File 19 | if f, err = os.Create(dst); err != nil { 20 | return errors.Wrapf(err, "astihttp: creating file %s failed", dst) 21 | } 22 | defer f.Close() 23 | 24 | // Download in writer 25 | if err = DownloadInWriter(ctx, c, src, f); err != nil { 26 | return errors.Wrap(err, "astihttp: downloading in writer failed") 27 | } 28 | return 29 | } 30 | 31 | // DownloadInWriter is a cancellable function that downloads a src into a writer using a specific *http.Client 32 | func DownloadInWriter(ctx context.Context, c *http.Client, src string, dst io.Writer) (err error) { 33 | // Send request 34 | var resp *http.Response 35 | if resp, err = c.Get(src); err != nil { 36 | return errors.Wrapf(err, "astihttp: getting %s failed", src) 37 | } 38 | defer resp.Body.Close() 39 | 40 | // Validate status code 41 | if resp.StatusCode != http.StatusOK { 42 | return fmt.Errorf("astihttp: getting %s returned %d status code", src, resp.StatusCode) 43 | } 44 | 45 | // Copy 46 | if _, err = astiio.Copy(ctx, resp.Body, dst); err != nil { 47 | return errors.Wrapf(err, "astihttp: copying content from %s to writer failed", src) 48 | } 49 | return 50 | } 51 | -------------------------------------------------------------------------------- /http/middleware.go: -------------------------------------------------------------------------------- 1 | package astihttp 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "strings" 7 | "time" 8 | 9 | "github.com/asticode/go-astilog" 10 | "github.com/julienschmidt/httprouter" 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | // ChainMiddlewares chains middlewares 15 | func ChainMiddlewares(h http.Handler, ms ...Middleware) http.Handler { 16 | return ChainMiddlewaresWithPrefix(h, []string{}, ms...) 17 | } 18 | 19 | // ChainMiddlewaresWithPrefix chains middlewares if one of prefixes is present 20 | func ChainMiddlewaresWithPrefix(h http.Handler, prefixes []string, ms ...Middleware) http.Handler { 21 | for _, m := range ms { 22 | if len(prefixes) == 0 { 23 | h = m(h) 24 | } else { 25 | t := h 26 | h = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { 27 | for _, prefix := range prefixes { 28 | if strings.HasPrefix(r.URL.EscapedPath(), prefix) { 29 | m(t).ServeHTTP(rw, r) 30 | return 31 | } 32 | } 33 | t.ServeHTTP(rw, r) 34 | }) 35 | } 36 | } 37 | return h 38 | } 39 | 40 | // ChainRouterMiddlewares chains router middlewares 41 | func ChainRouterMiddlewares(h httprouter.Handle, ms ...RouterMiddleware) httprouter.Handle { 42 | return ChainRouterMiddlewaresWithPrefix(h, []string{}, ms...) 43 | } 44 | 45 | // ChainRouterMiddlewares chains router middlewares if one of prefixes is present 46 | func ChainRouterMiddlewaresWithPrefix(h httprouter.Handle, prefixes []string, ms ...RouterMiddleware) httprouter.Handle { 47 | for _, m := range ms { 48 | if len(prefixes) == 0 { 49 | h = m(h) 50 | } else { 51 | t := h 52 | h = func(rw http.ResponseWriter, r *http.Request, p httprouter.Params) { 53 | for _, prefix := range prefixes { 54 | if strings.HasPrefix(r.URL.EscapedPath(), prefix) { 55 | m(t)(rw, r, p) 56 | return 57 | } 58 | } 59 | t(rw, r, p) 60 | } 61 | } 62 | } 63 | return h 64 | } 65 | 66 | // Middleware represents a middleware 67 | type Middleware func(http.Handler) http.Handler 68 | 69 | // RouterMiddleware represents a router middleware 70 | type RouterMiddleware func(httprouter.Handle) httprouter.Handle 71 | 72 | func handleBasicAuth(username, password string, rw http.ResponseWriter, r *http.Request) bool { 73 | if len(username) > 0 && len(password) > 0 { 74 | if u, p, ok := r.BasicAuth(); !ok || u != username || p != password { 75 | rw.Header().Set("WWW-Authenticate", "Basic Realm=Please enter your credentials") 76 | rw.WriteHeader(http.StatusUnauthorized) 77 | return true 78 | } 79 | } 80 | return false 81 | } 82 | 83 | // MiddlewareBasicAuth adds basic HTTP auth to a handler 84 | func MiddlewareBasicAuth(username, password string) Middleware { 85 | return func(h http.Handler) http.Handler { 86 | return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { 87 | // Basic auth 88 | if handleBasicAuth(username, password, rw, r) { 89 | return 90 | } 91 | 92 | // Next handler 93 | h.ServeHTTP(rw, r) 94 | }) 95 | } 96 | } 97 | 98 | // RouterMiddlewareBasicAuth adds basic HTTP auth to a router handler 99 | func RouterMiddlewareBasicAuth(username, password string) RouterMiddleware { 100 | return func(h httprouter.Handle) httprouter.Handle { 101 | return func(rw http.ResponseWriter, r *http.Request, p httprouter.Params) { 102 | // Basic auth 103 | if handleBasicAuth(username, password, rw, r) { 104 | return 105 | } 106 | 107 | // Next handler 108 | h(rw, r, p) 109 | } 110 | } 111 | } 112 | 113 | func handleContentType(contentType string, rw http.ResponseWriter) { 114 | rw.Header().Set("Content-Type", contentType) 115 | } 116 | 117 | // MiddlewareContentType adds a content type to a handler 118 | func MiddlewareContentType(contentType string) Middleware { 119 | return func(h http.Handler) http.Handler { 120 | return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { 121 | // Content type 122 | handleContentType(contentType, rw) 123 | 124 | // Next handler 125 | h.ServeHTTP(rw, r) 126 | }) 127 | } 128 | } 129 | 130 | // RouterMiddlewareContentType adds a content type to a router handler 131 | func RouterMiddlewareContentType(contentType string) RouterMiddleware { 132 | return func(h httprouter.Handle) httprouter.Handle { 133 | return func(rw http.ResponseWriter, r *http.Request, p httprouter.Params) { 134 | // Content type 135 | handleContentType(contentType, rw) 136 | 137 | // Next handler 138 | h(rw, r, p) 139 | } 140 | } 141 | } 142 | 143 | func handleHeaders(vs map[string]string, rw http.ResponseWriter) { 144 | for k, v := range vs { 145 | rw.Header().Set(k, v) 146 | } 147 | } 148 | 149 | // MiddlewareHeaders adds headers to a handler 150 | func MiddlewareHeaders(vs map[string]string) Middleware { 151 | return func(h http.Handler) http.Handler { 152 | return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { 153 | // Add headers 154 | handleHeaders(vs, rw) 155 | 156 | // Next handler 157 | h.ServeHTTP(rw, r) 158 | }) 159 | } 160 | } 161 | 162 | // RouterMiddlewareHeaders adds headers to a router handler 163 | func RouterMiddlewareHeaders(vs map[string]string) RouterMiddleware { 164 | return func(h httprouter.Handle) httprouter.Handle { 165 | return func(rw http.ResponseWriter, r *http.Request, p httprouter.Params) { 166 | // Add headers 167 | handleHeaders(vs, rw) 168 | 169 | // Next handler 170 | h(rw, r, p) 171 | } 172 | } 173 | } 174 | 175 | func handleTimeout(timeout time.Duration, rw http.ResponseWriter, fn func()) { 176 | // Init context 177 | ctx, cancel := context.WithTimeout(context.Background(), timeout) 178 | defer cancel() 179 | 180 | // Serve 181 | var done = make(chan bool) 182 | go func() { 183 | fn() 184 | done <- true 185 | }() 186 | 187 | // Wait for done or timeout 188 | for { 189 | select { 190 | case <-ctx.Done(): 191 | astilog.Error(errors.Wrap(ctx.Err(), "astihttp: serving HTTP failed")) 192 | rw.WriteHeader(http.StatusGatewayTimeout) 193 | return 194 | case <-done: 195 | return 196 | } 197 | } 198 | } 199 | 200 | // MiddlewareTimeout adds a timeout to a handler 201 | func MiddlewareTimeout(timeout time.Duration) Middleware { 202 | return func(h http.Handler) http.Handler { 203 | return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { 204 | handleTimeout(timeout, rw, func() { h.ServeHTTP(rw, r) }) 205 | }) 206 | } 207 | } 208 | 209 | // RouterMiddlewareTimeout adds a timeout to a router handler 210 | func RouterMiddlewareTimeout(timeout time.Duration) RouterMiddleware { 211 | return func(h httprouter.Handle) httprouter.Handle { 212 | return func(rw http.ResponseWriter, r *http.Request, p httprouter.Params) { 213 | handleTimeout(timeout, rw, func() { h(rw, r, p) }) 214 | } 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /http/sender.go: -------------------------------------------------------------------------------- 1 | package astihttp 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "net/http" 8 | "time" 9 | 10 | astilog "github.com/asticode/go-astilog" 11 | "github.com/pkg/errors" 12 | "golang.org/x/net/context/ctxhttp" 13 | ) 14 | 15 | // Sender represents an object capable of sending http requests 16 | type Sender struct { 17 | client *http.Client 18 | retryFunc RetryFunc 19 | retryMax int 20 | retrySleep time.Duration 21 | } 22 | 23 | // RetryFunc is a function that decides whether to retry the request 24 | type RetryFunc func(name string, resp *http.Response) bool 25 | 26 | func defaultRetryFunc(name string, resp *http.Response) bool { 27 | if resp.StatusCode >= http.StatusInternalServerError { 28 | astilog.Debugf("astihttp: invalid status code %d when sending %s", resp.StatusCode, name) 29 | return true 30 | } 31 | return false 32 | } 33 | 34 | // SenderOptions represents sender options 35 | type SenderOptions struct { 36 | Client *http.Client 37 | RetryFunc RetryFunc 38 | RetryMax int 39 | RetrySleep time.Duration 40 | } 41 | 42 | // NewSender creates a new sender 43 | func NewSender(o SenderOptions) (s *Sender) { 44 | s = &Sender{ 45 | client: o.Client, 46 | retryFunc: o.RetryFunc, 47 | retryMax: o.RetryMax, 48 | retrySleep: o.RetrySleep, 49 | } 50 | if s.client == nil { 51 | s.client = &http.Client{} 52 | } 53 | if s.retryFunc == nil { 54 | s.retryFunc = defaultRetryFunc 55 | } 56 | return 57 | } 58 | 59 | // Send sends a new *http.Request 60 | func (s *Sender) Send(req *http.Request) (resp *http.Response, err error) { 61 | return s.ExecWithRetry(fmt.Sprintf("%s request to %s", req.Method, req.URL), func() (*http.Response, error) { return s.client.Do(req) }) 62 | } 63 | 64 | // SendCtx sends a new *http.Request with a context 65 | func (s *Sender) SendCtx(ctx context.Context, req *http.Request) (resp *http.Response, err error) { 66 | return s.ExecWithRetry(fmt.Sprintf("%s request to %s", req.Method, req.URL), func() (*http.Response, error) { return ctxhttp.Do(ctx, s.client, req) }) 67 | } 68 | 69 | // ExecWithRetry handles retrying when fetching a response 70 | // name is used for logging purposes only 71 | func (s *Sender) ExecWithRetry(name string, fn func() (*http.Response, error)) (resp *http.Response, err error) { 72 | // Loop 73 | // We start at retryMax + 1 so that it runs at least once even if retryMax == 0 74 | tries := 0 75 | for retriesLeft := s.retryMax + 1; retriesLeft > 0; retriesLeft-- { 76 | // Get request name 77 | nr := fmt.Sprintf("%s (%d/%d)", name, s.retryMax-retriesLeft+2, s.retryMax+1) 78 | tries++ 79 | 80 | // Send request 81 | var retry bool 82 | astilog.Debugf("astihttp: sending %s", nr) 83 | if resp, err = fn(); err != nil { 84 | // If error is temporary, retry 85 | if netError, ok := err.(net.Error); ok && netError.Temporary() { 86 | astilog.Debugf("astihttp: temporary error when sending %s", nr) 87 | retry = true 88 | } else { 89 | err = errors.Wrapf(err, "astihttp: sending %s failed", nr) 90 | return 91 | } 92 | } 93 | 94 | // Retry 95 | if retry || s.retryFunc(nr, resp) { 96 | if retriesLeft > 1 { 97 | astilog.Debugf("astihttp: sleeping %s and retrying... (%d retries left)", s.retrySleep, retriesLeft-1) 98 | time.Sleep(s.retrySleep) 99 | } 100 | continue 101 | } 102 | 103 | // Return if conditions for retrying were not met 104 | return 105 | } 106 | 107 | // Max retries limit reached 108 | err = fmt.Errorf("astihttp: sending %s failed after %d tries", name, tries) 109 | return 110 | } 111 | -------------------------------------------------------------------------------- /http/server.go: -------------------------------------------------------------------------------- 1 | package astihttp 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "net/http" 7 | 8 | "github.com/asticode/go-astilog" 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | func Serve(ctx context.Context, h http.Handler, addr string, fn func(a net.Addr)) (err error) { 13 | // Create listener 14 | var l net.Listener 15 | if l, err = net.Listen("tcp", addr); err != nil { 16 | err = errors.Wrap(err, "astihttp: net.Listen failed") 17 | return 18 | } 19 | defer l.Close() 20 | 21 | // Create server 22 | astilog.Debugf("astihttp: serving on %s", l.Addr()) 23 | srv := &http.Server{Handler: h} 24 | defer func() { 25 | astilog.Debugf("astihttp: shutting down server on %s", l.Addr()) 26 | if err := srv.Shutdown(ctx); err != nil { 27 | astilog.Error(errors.Wrapf(err, "astihttp: shutting down server on %s failed", l.Addr())) 28 | } 29 | }() 30 | 31 | // Serve 32 | var chanDone = make(chan error) 33 | go func() { 34 | if err := srv.Serve(l); err != nil { 35 | chanDone <- err 36 | } 37 | }() 38 | 39 | // Execute custom callback 40 | if fn != nil { 41 | fn(l.Addr()) 42 | } 43 | 44 | // Wait for context or chanDone to be done 45 | select { 46 | case <-ctx.Done(): 47 | if ctx.Err() != context.Canceled { 48 | err = errors.Wrap(err, "astihttp: context error") 49 | } 50 | return 51 | case err = <-chanDone: 52 | if err != nil { 53 | err = errors.Wrap(err, "astihttp: serving failed") 54 | } 55 | return 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /image/color.go: -------------------------------------------------------------------------------- 1 | package astiimage 2 | 3 | import ( 4 | "image/color" 5 | "strconv" 6 | 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | // RGBA wraps color.RGBA 11 | type RGBA struct { 12 | color.RGBA 13 | } 14 | 15 | // NewRGBA creates a new RGBA color 16 | func NewRGBA(a, b, g, r uint8) *RGBA { 17 | return &RGBA{RGBA: color.RGBA{ 18 | A: a, 19 | B: b, 20 | G: g, 21 | R: r, 22 | }} 23 | } 24 | 25 | // UnmarshalText implements the encoding.TextUnmarshaler interface 26 | func (r *RGBA) UnmarshalText(i []byte) (err error) { 27 | var p uint64 28 | if p, err = strconv.ParseUint(string(i), 16, 32); err != nil { 29 | err = errors.Wrapf(err, "astiimage: parsing uint %s failed", i) 30 | return 31 | } 32 | r.R = uint8(p & 0xff) 33 | r.G = uint8(p >> 8 & 0xff) 34 | r.B = uint8(p >> 16 & 0xff) 35 | r.A = uint8(p >> 24 & 0xff) 36 | return 37 | } 38 | -------------------------------------------------------------------------------- /image/color_test.go: -------------------------------------------------------------------------------- 1 | package astiimage 2 | 3 | import ( 4 | "image/color" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestRGBA(t *testing.T) { 11 | c := &RGBA{} 12 | err := c.UnmarshalText([]byte("12345678")) 13 | assert.NoError(t, err) 14 | assert.Equal(t, RGBA{RGBA: color.RGBA{R: 0x78, G: 0x56, B: 0x34, A: 0x12}}, *c) 15 | err = c.UnmarshalText([]byte("9ABCDEFF")) 16 | assert.NoError(t, err) 17 | assert.Equal(t, RGBA{RGBA: color.RGBA{R: 0xff, G: 0xde, B: 0xbc, A: 0x9a}}, *c) 18 | } 19 | -------------------------------------------------------------------------------- /io/copy.go: -------------------------------------------------------------------------------- 1 | package astiio 2 | 3 | import ( 4 | "context" 5 | "io" 6 | ) 7 | 8 | // Copy represents a cancellable copy 9 | func Copy(ctx context.Context, src io.Reader, dst io.Writer) (int64, error) { 10 | return io.Copy(dst, NewReader(ctx, src)) 11 | } 12 | -------------------------------------------------------------------------------- /io/copy_test.go: -------------------------------------------------------------------------------- 1 | package astiio_test 2 | 3 | import ( 4 | "bytes" 5 | "sync" 6 | "testing" 7 | 8 | "github.com/asticode/go-astitools/io" 9 | "github.com/stretchr/testify/assert" 10 | "golang.org/x/net/context" 11 | ) 12 | 13 | // MockedReader is a mocked io.Reader 14 | type MockedReader struct { 15 | buf *bytes.Buffer 16 | infinite bool 17 | } 18 | 19 | // NewMockedReader creates a new mocked reader 20 | func NewMockedReader(i string, infinite bool) MockedReader { 21 | return MockedReader{buf: bytes.NewBuffer([]byte(i)), infinite: infinite} 22 | } 23 | 24 | // Read allows MockedReader to implement the io.Reader interface 25 | func (r MockedReader) Read(p []byte) (n int, err error) { 26 | if r.infinite { 27 | return 28 | } 29 | n, err = r.buf.Read(p) 30 | return 31 | } 32 | 33 | func TestCopy(t *testing.T) { 34 | // Init 35 | var w = &bytes.Buffer{} 36 | var r1, r2 = NewMockedReader("testiocopy", true), NewMockedReader("testiocopy", false) 37 | 38 | // Test cancel 39 | var nw int64 40 | var err error 41 | var ctx, cancel = context.WithCancel(context.Background()) 42 | var wg = &sync.WaitGroup{} 43 | wg.Add(1) 44 | go func() { 45 | defer wg.Done() 46 | nw, err = astiio.Copy(ctx, r1, w) 47 | }() 48 | cancel() 49 | wg.Wait() 50 | assert.EqualError(t, err, "context canceled") 51 | 52 | // Test success 53 | w.Reset() 54 | ctx = context.Background() 55 | nw, err = astiio.Copy(ctx, r2, w) 56 | assert.NoError(t, err) 57 | assert.Equal(t, int64(10), nw) 58 | assert.Equal(t, "testiocopy", w.String()) 59 | } 60 | -------------------------------------------------------------------------------- /io/linearizer.go: -------------------------------------------------------------------------------- 1 | package astiio 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "sync" 7 | "time" 8 | ) 9 | 10 | // Linearizer represents an object capable of linearizing data coming from an io.Reader as packets 11 | type Linearizer struct { 12 | bufferSize int 13 | bytesPool sync.Pool 14 | cancel context.CancelFunc 15 | ctx context.Context 16 | events []*event 17 | eventsSize int 18 | md sync.Mutex // Lock dispatch 19 | me sync.Mutex // Locks events 20 | mr sync.Mutex // Locks read 21 | r io.Reader 22 | } 23 | 24 | // event represents an event 25 | type event struct { 26 | b []byte 27 | err error 28 | n int 29 | p int 30 | } 31 | 32 | // NewLinearizer creates a new linearizer that will read readSize bytes at each iteration, write it in its internal 33 | // buffer capped at bufferSize bytes and allow reading this linearized data. 34 | func NewLinearizer(ctx context.Context, r io.Reader, readSize, bufferSize int) (l *Linearizer) { 35 | l = &Linearizer{ 36 | bufferSize: bufferSize, 37 | bytesPool: sync.Pool{New: func() interface{} { return make([]byte, readSize) }}, 38 | r: r, 39 | } 40 | l.ctx, l.cancel = context.WithCancel(ctx) 41 | return 42 | } 43 | 44 | // Close implements the io.Closer interface 45 | func (l *Linearizer) Close() error { 46 | l.cancel() 47 | if c, ok := l.r.(io.Closer); ok { 48 | return c.Close() 49 | } 50 | return nil 51 | } 52 | 53 | // Start reads the reader and dispatches events accordingly 54 | func (l *Linearizer) Start() { 55 | for { 56 | // Check context error 57 | if l.ctx.Err() != nil { 58 | l.md.Lock() 59 | l.dispatchEvent(&event{err: l.ctx.Err()}) 60 | return 61 | } 62 | 63 | // Get bytes from pool 64 | var b = l.bytesPool.Get().([]byte) 65 | 66 | // Read 67 | n, err := l.r.Read(b) 68 | if err != nil { 69 | l.md.Lock() 70 | l.dispatchEvent(&event{err: err}) 71 | return 72 | } 73 | 74 | // Dispatch event in a go routine so that it doesn't block the read 75 | l.md.Lock() 76 | go l.dispatchEvent(&event{b: b, n: n}) 77 | } 78 | } 79 | 80 | // dispatchEvent dispatches an event if it doesn't make the buffer overflow based on the bufferSize 81 | // Assumption is made that l.md is locked 82 | func (l *Linearizer) dispatchEvent(e *event) { 83 | defer l.md.Unlock() 84 | l.me.Lock() 85 | defer l.me.Unlock() 86 | if e.n+l.eventsSize > l.bufferSize { 87 | return 88 | } 89 | l.events = append(l.events, e) 90 | l.eventsSize += e.n 91 | } 92 | 93 | // Read implements the io.Reader interface 94 | func (l *Linearizer) Read(b []byte) (n int, err error) { 95 | // Only one read at a time 96 | l.mr.Lock() 97 | defer l.mr.Unlock() 98 | 99 | // Loop until there's enough data to read or there's an error 100 | for { 101 | // Check events size 102 | l.me.Lock() 103 | if l.eventsSize >= len(b) { 104 | // Loop in events 105 | for idx := 0; idx < len(l.events); idx++ { 106 | // Copy bytes 107 | e := l.events[idx] 108 | if len(b)-n < e.n-e.p { 109 | // Copy a part of the bytes 110 | copy(b[n:], e.b[e.p:len(b)-n+e.p]) 111 | e.p += len(b) - n 112 | n += len(b) - n 113 | } else { 114 | // Copy the remainder of the bytes 115 | copy(b[n:], e.b[e.p:e.n]) 116 | n += e.n - e.p 117 | 118 | // Put bytes back in pool 119 | l.bytesPool.Put(e.b) 120 | 121 | // Remove event 122 | l.events = append(l.events[:idx], l.events[idx+1:]...) 123 | idx-- 124 | } 125 | 126 | // All the bytes have been read 127 | if n == len(b) { 128 | break 129 | } 130 | } 131 | l.eventsSize -= n 132 | l.me.Unlock() 133 | return 134 | } 135 | 136 | // Process error in last event 137 | if len(l.events) > 0 { 138 | if err = l.events[len(l.events)-1].err; err != nil { 139 | l.me.Unlock() 140 | return 141 | } 142 | } 143 | l.me.Unlock() 144 | 145 | // Wait for 1ms 146 | time.Sleep(time.Millisecond) 147 | } 148 | return 149 | } 150 | -------------------------------------------------------------------------------- /io/linearizer_test.go: -------------------------------------------------------------------------------- 1 | package astiio_test 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "strconv" 7 | "testing" 8 | 9 | "github.com/asticode/go-astitools/io" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | type linearizerReader struct { 14 | closed bool 15 | count int 16 | } 17 | 18 | func (r *linearizerReader) Close() error { 19 | r.closed = true 20 | return nil 21 | } 22 | 23 | func (r *linearizerReader) Read(b []byte) (n int, err error) { 24 | if r.count == 3 { 25 | err = io.EOF 26 | return 27 | } 28 | b[0] = byte(strconv.Itoa(r.count)[0]) 29 | b[1] = byte('t') 30 | b[2] = byte('e') 31 | b[3] = byte('s') 32 | b[4] = byte('t') 33 | b[5] = byte(strconv.Itoa(r.count)[0]) 34 | n = 6 35 | r.count++ 36 | return 37 | } 38 | 39 | func TestLinearizer(t *testing.T) { 40 | pr := &linearizerReader{} 41 | p := astiio.NewLinearizer(context.Background(), pr, 10, 30) 42 | go p.Start() 43 | b := make([]byte, 3) 44 | n, err := p.Read(b) 45 | assert.NoError(t, err) 46 | assert.Equal(t, 3, n) 47 | assert.Equal(t, []byte("0te"), b) 48 | n, err = p.Read(b) 49 | assert.NoError(t, err) 50 | assert.Equal(t, 3, n) 51 | assert.Equal(t, []byte("st0"), b) 52 | n, err = p.Read(b) 53 | assert.NoError(t, err) 54 | assert.Equal(t, 3, n) 55 | assert.Equal(t, []byte("1te"), b) 56 | n, err = p.Read(b) 57 | assert.NoError(t, err) 58 | assert.Equal(t, 3, n) 59 | assert.Equal(t, []byte("st1"), b) 60 | n, err = p.Read(b) 61 | assert.NoError(t, err) 62 | assert.Equal(t, 3, n) 63 | assert.Equal(t, []byte("2te"), b) 64 | n, err = p.Read(b) 65 | assert.NoError(t, err) 66 | assert.Equal(t, 3, n) 67 | assert.Equal(t, []byte("st2"), b) 68 | _, err = p.Read(b) 69 | assert.Error(t, err, io.EOF.Error()) 70 | } 71 | -------------------------------------------------------------------------------- /io/reader.go: -------------------------------------------------------------------------------- 1 | package astiio 2 | 3 | import ( 4 | "context" 5 | "io" 6 | ) 7 | 8 | // Reader represents a reader with a context 9 | type Reader struct { 10 | ctx context.Context 11 | reader io.Reader 12 | } 13 | 14 | // NewReader creates a new Reader 15 | func NewReader(ctx context.Context, r io.Reader) *Reader { 16 | return &Reader{ 17 | ctx: ctx, 18 | reader: r, 19 | } 20 | } 21 | 22 | // Read allows Reader to implement the io.Reader interface 23 | func (r *Reader) Read(p []byte) (n int, err error) { 24 | // Check context 25 | if err = r.ctx.Err(); err != nil { 26 | return 27 | } 28 | 29 | // Read 30 | return r.reader.Read(p) 31 | } 32 | -------------------------------------------------------------------------------- /limiter/bucket.go: -------------------------------------------------------------------------------- 1 | package astilimiter 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // Bucket represents a bucket 8 | type Bucket struct { 9 | cap int 10 | channelQuit chan bool 11 | count int 12 | period time.Duration 13 | } 14 | 15 | // newBucket creates a new bucket 16 | func newBucket(cap int, period time.Duration) (b *Bucket) { 17 | b = &Bucket{ 18 | cap: cap, 19 | channelQuit: make(chan bool), 20 | count: 0, 21 | period: period, 22 | } 23 | go b.tick() 24 | return 25 | } 26 | 27 | // Inc increments the bucket count 28 | func (b *Bucket) Inc() bool { 29 | if b.count >= b.cap { 30 | return false 31 | } 32 | b.count++ 33 | return true 34 | } 35 | 36 | // tick runs a ticker to purge the bucket 37 | func (b *Bucket) tick() { 38 | var t = time.NewTicker(b.period) 39 | defer t.Stop() 40 | for { 41 | select { 42 | case <-t.C: 43 | b.count = 0 44 | case <-b.channelQuit: 45 | return 46 | } 47 | } 48 | } 49 | 50 | // close closes the bucket properly 51 | func (b *Bucket) Close() { 52 | if b.channelQuit != nil { 53 | close(b.channelQuit) 54 | b.channelQuit = nil 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /limiter/bucket_test.go: -------------------------------------------------------------------------------- 1 | package astilimiter_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/asticode/go-astitools/limiter" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestBucket(t *testing.T) { 12 | var l = astilimiter.New() 13 | var b = l.Add("test", 2, time.Second) 14 | assert.True(t, b.Inc()) 15 | assert.True(t, b.Inc()) 16 | assert.False(t, b.Inc()) 17 | } 18 | -------------------------------------------------------------------------------- /limiter/limiter.go: -------------------------------------------------------------------------------- 1 | package astilimiter 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | // Limiter represents a limiter 9 | type Limiter struct { 10 | buckets map[string]*Bucket 11 | m *sync.Mutex // Locks buckets 12 | } 13 | 14 | // New creates a new limiter 15 | func New() *Limiter { 16 | return &Limiter{ 17 | buckets: make(map[string]*Bucket), 18 | m: &sync.Mutex{}, 19 | } 20 | } 21 | 22 | // Add adds a new bucket 23 | func (l *Limiter) Add(name string, cap int, period time.Duration) *Bucket { 24 | l.m.Lock() 25 | defer l.m.Unlock() 26 | if _, ok := l.buckets[name]; !ok { 27 | l.buckets[name] = newBucket(cap, period) 28 | } 29 | return l.buckets[name] 30 | } 31 | 32 | // Bucket retrieves a bucket from the limiter 33 | func (l *Limiter) Bucket(name string) (b *Bucket, ok bool) { 34 | l.m.Lock() 35 | defer l.m.Unlock() 36 | b, ok = l.buckets[name] 37 | return 38 | } 39 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package astitools 2 | -------------------------------------------------------------------------------- /map/map.go: -------------------------------------------------------------------------------- 1 | package astimap 2 | 3 | // Map represents a bi-directional map 4 | type Map struct { 5 | defaultA interface{} 6 | defaultB interface{} 7 | mapAToB map[interface{}]interface{} 8 | mapBToA map[interface{}]interface{} 9 | } 10 | 11 | // NewMap builds a new *Map 12 | func NewMap(defaultA, defaultB interface{}) *Map { 13 | return &Map{ 14 | defaultA: defaultA, 15 | defaultB: defaultB, 16 | mapAToB: make(map[interface{}]interface{}), 17 | mapBToA: make(map[interface{}]interface{}), 18 | } 19 | } 20 | 21 | // A retrieves a based on b 22 | func (m *Map) A(b interface{}) interface{} { 23 | if a, ok := m.mapBToA[b]; ok { 24 | return a 25 | } 26 | return m.defaultA 27 | } 28 | 29 | // B retrieves b based on a 30 | func (m *Map) B(a interface{}) interface{} { 31 | if b, ok := m.mapAToB[a]; ok { 32 | return b 33 | } 34 | return m.defaultB 35 | } 36 | 37 | // InA checks whether a exists 38 | func (m *Map) InA(a interface{}) (ok bool) { 39 | _, ok = m.mapAToB[a] 40 | return 41 | } 42 | 43 | // InB checks whether b exists 44 | func (m *Map) InB(b interface{}) (ok bool) { 45 | _, ok = m.mapBToA[b] 46 | return 47 | } 48 | 49 | // Set sets a key/value couple 50 | func (m *Map) Set(a, b interface{}) *Map { 51 | m.mapAToB[a] = b 52 | m.mapBToA[b] = a 53 | return m 54 | } 55 | -------------------------------------------------------------------------------- /os/checksum.go: -------------------------------------------------------------------------------- 1 | package astios 2 | 3 | import ( 4 | "crypto/sha1" 5 | "encoding/base64" 6 | "io" 7 | "os" 8 | 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | // Checksum computes the checksum of a file 13 | func Checksum(path string) (checksum string, err error) { 14 | // Open executable 15 | var f *os.File 16 | if f, err = os.Open(path); err != nil { 17 | err = errors.Wrapf(err, "opening %s failed", path) 18 | return 19 | } 20 | defer f.Close() 21 | 22 | // Compute checksum 23 | var h = sha1.New() 24 | if _, err = io.Copy(h, f); err != nil { 25 | err = errors.Wrap(err, "copying file to hasher failed") 26 | return 27 | } 28 | checksum = base64.StdEncoding.EncodeToString(h.Sum(nil)) 29 | return 30 | } 31 | -------------------------------------------------------------------------------- /os/checksum_test.go: -------------------------------------------------------------------------------- 1 | package astios 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestChecksum(t *testing.T) { 10 | c, err := Checksum("./testdata/checksum") 11 | assert.NoError(t, err) 12 | assert.Equal(t, "cRDtpNCeBiql5KOQsKVyrA0sAiA=", c) 13 | } 14 | -------------------------------------------------------------------------------- /os/copy.go: -------------------------------------------------------------------------------- 1 | package astios 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | 9 | "github.com/asticode/go-astitools/io" 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | // Copy is a cross partitions cancellable copy 14 | // If src is a file, dst must be the full path to file once copied 15 | // If src is a dir, dst must be the full path to the dir once copied 16 | func Copy(ctx context.Context, src, dst string) (err error) { 17 | // Check context 18 | if err = ctx.Err(); err != nil { 19 | return 20 | } 21 | 22 | // Stat src 23 | var statSrc os.FileInfo 24 | if statSrc, err = os.Stat(src); err != nil { 25 | err = errors.Wrapf(err, "stating %s failed", src) 26 | return 27 | } 28 | 29 | // Check context 30 | if err = ctx.Err(); err != nil { 31 | return 32 | } 33 | 34 | // Dir 35 | if statSrc.IsDir() { 36 | if err = filepath.Walk(src, func(path string, info os.FileInfo, errWalk error) (err error) { 37 | // Check error 38 | if errWalk != nil { 39 | err = errWalk 40 | return 41 | } 42 | 43 | // Do not process root 44 | if src == path { 45 | return 46 | } 47 | 48 | // Copy 49 | var p = filepath.Join(dst, strings.TrimPrefix(path, filepath.Clean(src))) 50 | if err = Copy(ctx, path, p); err != nil { 51 | err = errors.Wrapf(err, "copying %s to %s failed", path, p) 52 | return 53 | } 54 | return nil 55 | }); err != nil { 56 | return 57 | } 58 | return 59 | } 60 | 61 | // Open the source file 62 | var srcFile *os.File 63 | if srcFile, err = os.Open(src); err != nil { 64 | err = errors.Wrapf(err, "opening %s failed", src) 65 | return 66 | } 67 | defer srcFile.Close() 68 | 69 | // Create the destination folder 70 | if err = os.MkdirAll(filepath.Dir(dst), 0755); err != nil { 71 | err = errors.Wrapf(err, "mkdirall %s failed", filepath.Dir(dst)) 72 | return 73 | } 74 | 75 | // Check context 76 | if err = ctx.Err(); err != nil { 77 | return 78 | } 79 | 80 | // Create the destination file 81 | var dstFile *os.File 82 | if dstFile, err = os.Create(dst); err != nil { 83 | err = errors.Wrapf(err, "creating %s failed", dst) 84 | return 85 | } 86 | defer dstFile.Close() 87 | 88 | // Chmod using os.chmod instead of file.Chmod 89 | if err = os.Chmod(dst, statSrc.Mode()); err != nil { 90 | err = errors.Wrapf(err, "chmod %s %s failed", dst, statSrc.Mode()) 91 | return 92 | } 93 | 94 | // Check context 95 | if err = ctx.Err(); err != nil { 96 | return 97 | } 98 | 99 | // Copy the content 100 | if _, err = astiio.Copy(ctx, srcFile, dstFile); err != nil { 101 | err = errors.Wrapf(err, "copying content of %s to %s failed", srcFile.Name(), dstFile.Name()) 102 | return 103 | } 104 | 105 | return 106 | } 107 | -------------------------------------------------------------------------------- /os/copy_test.go: -------------------------------------------------------------------------------- 1 | package astios 2 | 3 | import ( 4 | "context" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | "testing" 9 | 10 | "github.com/pkg/errors" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestCopy(t *testing.T) { 15 | // Create temporary dir 16 | p, err := ioutil.TempDir("", "astitools_os_test_") 17 | if err != nil { 18 | t.Log(errors.Wrapf(err, "creating %s failed, skipping TestCopy", p)) 19 | return 20 | } 21 | 22 | // Make sure the dir is deleted 23 | defer func() { 24 | return 25 | if err = os.RemoveAll(p); err != nil { 26 | t.Log(errors.Wrapf(err, "removing %s failed", p)) 27 | } 28 | }() 29 | 30 | // Copy file 31 | err = Copy(context.Background(), "./testdata/copy/f", filepath.Join(p, "f")) 32 | assert.NoError(t, err) 33 | checkFile(t, filepath.Join(p, "f"), []byte("0")) 34 | 35 | // Copy dir 36 | err = Copy(context.Background(), "./testdata/copy/d", filepath.Join(p, "d")) 37 | assert.NoError(t, err) 38 | checkFile(t, filepath.Join(p, "d", "f1"), []byte("1")) 39 | checkFile(t, filepath.Join(p, "d", "d1", "f11"), []byte("2")) 40 | checkFile(t, filepath.Join(p, "d", "d2", "f21"), []byte("3")) 41 | checkFile(t, filepath.Join(p, "d", "d2", "d21", "f211"), []byte("4")) 42 | } 43 | 44 | func checkFile(t *testing.T, p string, c []byte) { 45 | b, err := ioutil.ReadFile(p) 46 | assert.NoError(t, err) 47 | assert.Equal(t, c, b) 48 | } 49 | -------------------------------------------------------------------------------- /os/dir.go: -------------------------------------------------------------------------------- 1 | package astios 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | // TempDir creates a temp dir 11 | func TempDir(prefix string) (path string, err error) { 12 | // Create temp file 13 | var f *os.File 14 | if f, err = ioutil.TempFile(os.TempDir(), prefix); err != nil { 15 | err = errors.Wrap(err, "creating temporary file failed") 16 | return 17 | } 18 | path = f.Name() 19 | 20 | // Close temp file 21 | if err = f.Close(); err != nil { 22 | err = errors.Wrapf(err, "closing file %s failed", path) 23 | return 24 | } 25 | 26 | // Delete temp file 27 | if err = os.Remove(path); err != nil { 28 | err = errors.Wrapf(err, "removing %s failed", path) 29 | return 30 | } 31 | 32 | // Create temp dir 33 | if err = os.MkdirAll(path, 0755); err != nil { 34 | err = errors.Wrapf(err, "mkdirall of %s failed", path) 35 | return 36 | } 37 | return 38 | } 39 | -------------------------------------------------------------------------------- /os/file.go: -------------------------------------------------------------------------------- 1 | package astios 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | // TempFile writes a content to a temp file 11 | func TempFile(i []byte) (name string, err error) { 12 | // Create temp file 13 | var f *os.File 14 | if f, err = ioutil.TempFile(os.TempDir(), "astitools"); err != nil { 15 | err = errors.Wrap(err, "creating temp file failed") 16 | return 17 | } 18 | name = f.Name() 19 | defer f.Close() 20 | 21 | // Write 22 | if _, err = f.Write(i); err != nil { 23 | err = errors.Wrapf(err, "writing to %s failed", name) 24 | return 25 | } 26 | return 27 | } 28 | -------------------------------------------------------------------------------- /os/move.go: -------------------------------------------------------------------------------- 1 | package astios 2 | 3 | import ( 4 | "context" 5 | "os" 6 | ) 7 | 8 | // Move is a cross partitions cancellable move even if files are on different partitions 9 | func Move(ctx context.Context, src, dst string) (err error) { 10 | // Check context 11 | if err = ctx.Err(); err != nil { 12 | return 13 | } 14 | 15 | // Copy 16 | if err = Copy(ctx, src, dst); err != nil { 17 | return 18 | } 19 | 20 | // Check context 21 | if err = ctx.Err(); err != nil { 22 | return 23 | } 24 | 25 | // Delete 26 | err = os.Remove(src) 27 | return 28 | } 29 | -------------------------------------------------------------------------------- /os/signals.go: -------------------------------------------------------------------------------- 1 | package astios 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "os/signal" 7 | "syscall" 8 | 9 | "github.com/asticode/go-astilog" 10 | ) 11 | 12 | type SignalsFunc func(s os.Signal) 13 | 14 | func HandleSignals(fn SignalsFunc) { 15 | ch := make(chan os.Signal, 1) 16 | signal.Notify(ch) 17 | for s := range ch { 18 | astilog.Debugf("astios: received signal %s", s) 19 | fn(s) 20 | } 21 | } 22 | 23 | func ContextSignalsFunc(c context.CancelFunc) SignalsFunc { 24 | return func(s os.Signal) { 25 | if s == syscall.SIGABRT || s == syscall.SIGINT || s == syscall.SIGQUIT || s == syscall.SIGTERM { 26 | c() 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /os/testdata/checksum: -------------------------------------------------------------------------------- 1 | 1234 -------------------------------------------------------------------------------- /os/testdata/copy/d/d1/f11: -------------------------------------------------------------------------------- 1 | 2 -------------------------------------------------------------------------------- /os/testdata/copy/d/d2/d21/f211: -------------------------------------------------------------------------------- 1 | 4 -------------------------------------------------------------------------------- /os/testdata/copy/d/d2/f21: -------------------------------------------------------------------------------- 1 | 3 -------------------------------------------------------------------------------- /os/testdata/copy/d/f1: -------------------------------------------------------------------------------- 1 | 1 -------------------------------------------------------------------------------- /os/testdata/copy/f: -------------------------------------------------------------------------------- 1 | 0 -------------------------------------------------------------------------------- /pcm/amplitude.go: -------------------------------------------------------------------------------- 1 | package astipcm 2 | 3 | import ( 4 | "math" 5 | ) 6 | 7 | func maxSample(bitDepth int) int { 8 | return int(math.Pow(2, float64(bitDepth))/2.0) - 1 9 | } 10 | 11 | func SampleToAmplitude(s int, bitDepth int, signed bool) (a float64) { 12 | // Get max 13 | max := maxSample(bitDepth) 14 | 15 | // Compute amplitude 16 | if signed { 17 | // Sample values are between -max <= x <= max 18 | // We need them to be 0 <= x <= 1 so we first make them be -0.5 <= x <= 0.5 and we add 0.5 19 | a = (float64(s) / (float64(max) * 2.0)) + 0.5 20 | } else { 21 | a = float64(s) / float64(max) 22 | } 23 | return 24 | } 25 | 26 | func AmplitudeToSample(a float64, bitDepth int, signed bool) (s int) { 27 | // Get max 28 | max := maxSample(bitDepth) 29 | 30 | // Compute sample 31 | if signed { 32 | s = int((a - 0.5) * (float64(max) * 2.0)) 33 | } else { 34 | s = int(a * float64(max)) 35 | } 36 | return 37 | } 38 | 39 | func AmplitudeToDB(a float64) float64 { 40 | return 20 * math.Log10(a) 41 | } 42 | 43 | func DBToAmplitude(db float64) float64 { 44 | return math.Pow(10.0, db*0.05) 45 | } 46 | 47 | // https://stackoverflow.com/questions/2445756/how-can-i-calculate-audio-db-level 48 | func SampleToDB(s int, bitDepth int, signed bool) float64 { 49 | return AmplitudeToDB(SampleToAmplitude(s, bitDepth, signed)) 50 | } 51 | 52 | func DBToSample(db float64, bitDepth int, signed bool) int { 53 | return AmplitudeToSample(DBToAmplitude(db), bitDepth, signed) 54 | } 55 | 56 | func Normalize(samples []int, bitDepth int) (o []int) { 57 | // Get max sample 58 | var m int 59 | for _, s := range samples { 60 | if v := int(math.Abs(float64(s))); v > m { 61 | m = v 62 | } 63 | } 64 | 65 | // Get max for bit depth 66 | max := maxSample(bitDepth) 67 | 68 | // Loop through samples 69 | for _, s := range samples { 70 | o = append(o, s*max/m) 71 | } 72 | return 73 | } 74 | -------------------------------------------------------------------------------- /pcm/amplitude_test.go: -------------------------------------------------------------------------------- 1 | package astipcm 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "os" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestMaxSample(t *testing.T) { 13 | assert.Equal(t, 127, maxSample(8)) 14 | assert.Equal(t, 32767, maxSample(16)) 15 | assert.Equal(t, 8388607, maxSample(24)) 16 | assert.Equal(t, 2147483647, maxSample(32)) 17 | } 18 | 19 | func TestSampleToAmplitude(t *testing.T) { 20 | // Signed boundaries 21 | assert.Equal(t, 1.0, SampleToAmplitude(maxSample(16), 16, true)) 22 | assert.Equal(t, 0.0, SampleToAmplitude(-maxSample(16), 16, true)) 23 | 24 | // Signed value 25 | assert.Equal(t, 0.5, SampleToAmplitude(0, 16, true)) 26 | 27 | // Unsigned boundaries 28 | assert.Equal(t, 1.0, SampleToAmplitude(maxSample(16), 16, false)) 29 | assert.Equal(t, 0.0, SampleToAmplitude(0, 16, false)) 30 | 31 | // Unsigned value 32 | assert.Equal(t, 0.499984740745262, SampleToAmplitude(maxSample(16)/2, 16, false)) 33 | } 34 | 35 | func TestAmplitudeToSample(t *testing.T) { 36 | // Signed boundaries 37 | assert.Equal(t, maxSample(16), AmplitudeToSample(1.0, 16, true)) 38 | assert.Equal(t, -maxSample(16), AmplitudeToSample(0.0, 16, true)) 39 | 40 | // Signed value 41 | assert.Equal(t, 0, AmplitudeToSample(0.5, 16, true)) 42 | 43 | // Unsigned boundaries 44 | assert.Equal(t, maxSample(16), AmplitudeToSample(1.0, 16, false)) 45 | assert.Equal(t, 0, AmplitudeToSample(0.0, 16, false)) 46 | 47 | // Unsigned value 48 | assert.Equal(t, maxSample(16)/2.0, AmplitudeToSample(0.499984740745262, 16, false)) 49 | } 50 | 51 | func TestAmplitudeToDB(t *testing.T) { 52 | assert.Equal(t, math.Inf(-1), AmplitudeToDB(0.0)) 53 | assert.Equal(t, -7.1309464702762515, AmplitudeToDB(0.44)) 54 | assert.Equal(t, 0.0, AmplitudeToDB(1.0)) 55 | } 56 | 57 | func TestDBToAmplitude(t *testing.T) { 58 | assert.Equal(t, 0.0, DBToAmplitude(math.Inf(-1))) 59 | assert.Equal(t, 0.44004794783598367, DBToAmplitude(-7.13)) 60 | assert.Equal(t, 1.0, DBToAmplitude(0.0)) 61 | } 62 | 63 | func TestSampleToDB(t *testing.T) { 64 | assert.Equal(t, -6.020599913279624, SampleToDB(0, 16, true)) 65 | assert.Equal(t, -2.4988630927505144, SampleToDB(maxSample(16)/2, 16, true)) 66 | } 67 | 68 | func TestDBToSample(t *testing.T) { 69 | assert.Equal(t, 0, DBToSample(-6.020599913279624, 16, true)) 70 | assert.Equal(t, maxSample(16)/2, DBToSample(-2.4988630927505144, 16, true)) 71 | } 72 | 73 | func TestNormalize(t *testing.T) { 74 | // Nothing to do 75 | i := []int{10000, maxSample(16), -10000} 76 | assert.Equal(t, i, Normalize(i, 16)) 77 | 78 | fmt.Fprintln(os.Stdout, "") 79 | 80 | // Normalize 81 | i = []int{10000, 0, -10000} 82 | assert.Equal(t, []int{32767, 0, -32767}, Normalize(i, 16)) 83 | } 84 | -------------------------------------------------------------------------------- /pcm/bit_depth.go: -------------------------------------------------------------------------------- 1 | package astipcm 2 | 3 | // ConvertBitDepth converts the bit depth 4 | func ConvertBitDepth(srcSample int, srcBitDepth, dstBitDepth int) (dstSample int, err error) { 5 | // Nothing to do 6 | if srcBitDepth == dstBitDepth { 7 | dstSample = srcSample 8 | return 9 | } 10 | 11 | // Convert 12 | if srcBitDepth < dstBitDepth { 13 | dstSample = srcSample << uint(dstBitDepth-srcBitDepth) 14 | } else { 15 | dstSample = srcSample >> uint(srcBitDepth-dstBitDepth) 16 | } 17 | return 18 | } 19 | -------------------------------------------------------------------------------- /pcm/bit_depth_test.go: -------------------------------------------------------------------------------- 1 | package astipcm 2 | 3 | import ( 4 | "testing" 5 | "github.com/stretchr/testify/assert" 6 | ) 7 | 8 | func TestBitDepth(t *testing.T) { 9 | // Nothing to do 10 | s, err := ConvertBitDepth(1>>8, 16, 16) 11 | assert.NoError(t, err) 12 | assert.Equal(t, 1>>8, s) 13 | 14 | // Src bit depth > Dst bit depth 15 | s, err = ConvertBitDepth(1>>24, 32, 16) 16 | assert.NoError(t, err) 17 | assert.Equal(t, 1>>8, s) 18 | 19 | // Src bit depth < Dst bit depth 20 | s, err = ConvertBitDepth(1>>8, 16, 32) 21 | assert.NoError(t, err) 22 | assert.Equal(t, 1>>24, s) 23 | } 24 | -------------------------------------------------------------------------------- /pcm/channels.go: -------------------------------------------------------------------------------- 1 | package astipcm 2 | 3 | import ( 4 | "github.com/pkg/errors" 5 | ) 6 | 7 | type ChannelsConverter struct { 8 | dstNumChannels int 9 | fn SampleFunc 10 | srcNumChannels int 11 | srcSamples int 12 | } 13 | 14 | func NewChannelsConverter(srcNumChannels, dstNumChannels int, fn SampleFunc) *ChannelsConverter { 15 | return &ChannelsConverter{ 16 | dstNumChannels: dstNumChannels, 17 | fn: fn, 18 | srcNumChannels: srcNumChannels, 19 | } 20 | } 21 | 22 | func (c *ChannelsConverter) Reset() { 23 | c.srcSamples = 0 24 | } 25 | 26 | func (c *ChannelsConverter) Add(i int) (err error) { 27 | // Forward sample 28 | if c.srcNumChannels == c.dstNumChannels { 29 | if err = c.fn(i); err != nil { 30 | err = errors.Wrap(err, "astipcm: handling sample failed") 31 | return 32 | } 33 | return 34 | } 35 | 36 | // Reset 37 | if c.srcSamples == c.srcNumChannels { 38 | c.srcSamples = 0 39 | } 40 | 41 | // Increment src samples 42 | c.srcSamples++ 43 | 44 | // Throw away data 45 | if c.srcNumChannels > c.dstNumChannels { 46 | // Throw away sample 47 | if c.srcSamples > c.dstNumChannels { 48 | return 49 | } 50 | 51 | // Custom 52 | if err = c.fn(i); err != nil { 53 | err = errors.Wrap(err, "astipcm: handling sample failed") 54 | return 55 | } 56 | return 57 | } 58 | 59 | // Store 60 | var ss []int 61 | if c.srcSamples < c.srcNumChannels { 62 | ss = []int{i} 63 | } else { 64 | // Repeat data 65 | for idx := c.srcNumChannels; idx <= c.dstNumChannels; idx++ { 66 | ss = append(ss, i) 67 | } 68 | } 69 | 70 | // Loop through samples 71 | for _, s := range ss { 72 | // Custom 73 | if err = c.fn(s); err != nil { 74 | err = errors.Wrap(err, "astipcm: handling sample failed") 75 | return 76 | } 77 | } 78 | return 79 | } 80 | -------------------------------------------------------------------------------- /pcm/channels_test.go: -------------------------------------------------------------------------------- 1 | package astipcm 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestChannelsConverter(t *testing.T) { 10 | // Create input 11 | var i []int 12 | for idx := 0; idx < 20; idx++ { 13 | i = append(i, idx+1) 14 | } 15 | 16 | // Create sample func 17 | var o []int 18 | var sampleFunc = func(s int) (err error) { 19 | o = append(o, s) 20 | return 21 | } 22 | 23 | // Nothing to do 24 | c := NewChannelsConverter(3, 3, sampleFunc) 25 | for _, s := range i { 26 | c.Add(s) 27 | } 28 | assert.Equal(t, i, o) 29 | 30 | // Throw away data 31 | o = []int{} 32 | c = NewChannelsConverter(3, 1, sampleFunc) 33 | for _, s := range i { 34 | c.Add(s) 35 | } 36 | assert.Equal(t, []int{1, 4, 7, 10, 13, 16, 19}, o) 37 | 38 | // Repeat data 39 | o = []int{} 40 | c = NewChannelsConverter(1, 2, sampleFunc) 41 | for _, s := range i { 42 | c.Add(s) 43 | } 44 | assert.Equal(t, []int{1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, 14, 14, 15, 15, 16, 16, 17, 17, 18, 18, 19, 19, 20, 20}, o) 45 | } 46 | -------------------------------------------------------------------------------- /pcm/level.go: -------------------------------------------------------------------------------- 1 | package astipcm 2 | 3 | import "math" 4 | 5 | // AudioLevel computes the audio level of samples 6 | // https://dsp.stackexchange.com/questions/2951/loudness-of-pcm-stream 7 | // https://dsp.stackexchange.com/questions/290/getting-loudness-of-a-track-with-rms?noredirect=1&lq=1 8 | func AudioLevel(samples []int) float64 { 9 | // Compute sum of square values 10 | var sum float64 11 | for _, s := range samples { 12 | sum += math.Pow(float64(s), 2) 13 | } 14 | 15 | // Square root 16 | return math.Sqrt(sum / float64(len(samples))) 17 | } 18 | -------------------------------------------------------------------------------- /pcm/level_test.go: -------------------------------------------------------------------------------- 1 | package astipcm 2 | 3 | import ( 4 | "testing" 5 | "github.com/stretchr/testify/assert" 6 | ) 7 | 8 | func TestAudioLevel(t *testing.T) { 9 | assert.Equal(t, 2.160246899469287, AudioLevel([]int{1, 2, 3})) 10 | } 11 | -------------------------------------------------------------------------------- /pcm/sample_rate.go: -------------------------------------------------------------------------------- 1 | package astipcm 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/pkg/errors" 7 | ) 8 | 9 | type SampleFunc func(s int) error 10 | 11 | type SampleRateConverter struct { 12 | b [][]int 13 | dstSampleRate int 14 | fn SampleFunc 15 | numChannels int 16 | numChannelsProcessed int 17 | numSamplesOutputed int 18 | numSamplesProcessed int 19 | srcSampleRate int 20 | } 21 | 22 | func NewSampleRateConverter(srcSampleRate, dstSampleRate, numChannels int, fn SampleFunc) *SampleRateConverter { 23 | return &SampleRateConverter{ 24 | b: make([][]int, numChannels), 25 | dstSampleRate: dstSampleRate, 26 | fn: fn, 27 | numChannels: numChannels, 28 | srcSampleRate: srcSampleRate, 29 | } 30 | } 31 | 32 | func (c *SampleRateConverter) Reset() { 33 | c.b = make([][]int, c.numChannels) 34 | c.numChannelsProcessed = 0 35 | c.numSamplesOutputed = 0 36 | c.numSamplesProcessed = 0 37 | } 38 | 39 | func (c *SampleRateConverter) Add(i int) (err error) { 40 | // Forward sample 41 | if c.srcSampleRate == c.dstSampleRate { 42 | if err = c.fn(i); err != nil { 43 | err = errors.Wrap(err, "astipcm: handling sample failed") 44 | return 45 | } 46 | return 47 | } 48 | 49 | // Increment num channels processed 50 | c.numChannelsProcessed++ 51 | 52 | // Reset num channels processed 53 | if c.numChannelsProcessed > c.numChannels { 54 | c.numChannelsProcessed = 1 55 | } 56 | 57 | // Only increment num samples processed if all channels have been processed 58 | if c.numChannelsProcessed == c.numChannels { 59 | c.numSamplesProcessed++ 60 | } 61 | 62 | // Append sample to buffer 63 | c.b[c.numChannelsProcessed-1] = append(c.b[c.numChannelsProcessed-1], i) 64 | 65 | // Throw away data 66 | if c.srcSampleRate > c.dstSampleRate { 67 | // Make sure to always keep the first sample but do nothing until we have all channels or target sample has been 68 | // reached 69 | if (c.numSamplesOutputed > 0 && float64(c.numSamplesProcessed) < 1.0+float64(c.numSamplesOutputed)*float64(c.srcSampleRate)/float64(c.dstSampleRate)) || c.numChannelsProcessed < c.numChannels { 70 | return 71 | } 72 | 73 | // Loop through channels 74 | for idx, b := range c.b { 75 | // Merge samples 76 | var s int 77 | for _, v := range b { 78 | s += v 79 | } 80 | s /= len(b) 81 | 82 | // Reset buffer 83 | c.b[idx] = []int{} 84 | 85 | // Custom 86 | if err = c.fn(s); err != nil { 87 | err = errors.Wrap(err, "astipcm: handling sample failed") 88 | return 89 | } 90 | } 91 | 92 | // Increment num samples outputed 93 | c.numSamplesOutputed++ 94 | return 95 | } 96 | 97 | // Do nothing until we have all channels 98 | if c.numChannelsProcessed < c.numChannels { 99 | return 100 | } 101 | 102 | // Repeat data 103 | for c.numSamplesOutputed == 0 || float64(c.numSamplesProcessed)+1.0 > 1.0+float64(c.numSamplesOutputed)*float64(c.srcSampleRate)/float64(c.dstSampleRate) { 104 | // Loop through channels 105 | for _, b := range c.b { 106 | // Invalid length 107 | if len(b) != 1 { 108 | err = fmt.Errorf("astipcm: invalid buffer item length %d", len(b)) 109 | return 110 | } 111 | 112 | // Custom 113 | if err = c.fn(b[0]); err != nil { 114 | err = errors.Wrap(err, "astipcm: handling sample failed") 115 | return 116 | } 117 | } 118 | 119 | // Increment num samples outputed 120 | c.numSamplesOutputed++ 121 | } 122 | 123 | // Reset buffer 124 | c.b = make([][]int, c.numChannels) 125 | return 126 | } 127 | -------------------------------------------------------------------------------- /pcm/sample_rate_test.go: -------------------------------------------------------------------------------- 1 | package astipcm 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestSampleRateConverter(t *testing.T) { 10 | // Create input 11 | var i []int 12 | for idx := 0; idx < 20; idx++ { 13 | i = append(i, idx+1) 14 | } 15 | 16 | // Create sample func 17 | var o []int 18 | var sampleFunc = func(s int) (err error) { 19 | o = append(o, s) 20 | return 21 | } 22 | 23 | // Nothing to do 24 | c := NewSampleRateConverter(1, 1, 1, sampleFunc) 25 | for _, s := range i { 26 | c.Add(s) 27 | } 28 | assert.Equal(t, i, o) 29 | 30 | // Simple src sample rate > dst sample rate 31 | o = []int{} 32 | c = NewSampleRateConverter(5, 3, 1, sampleFunc) 33 | for _, s := range i { 34 | c.Add(s) 35 | } 36 | assert.Equal(t, []int{1, 2, 4, 6, 7, 9, 11, 12, 14, 16, 17, 19}, o) 37 | 38 | // Multi channels 39 | o = []int{} 40 | c = NewSampleRateConverter(4, 2, 2, sampleFunc) 41 | for _, s := range i { 42 | c.Add(s) 43 | } 44 | assert.Equal(t, []int{1, 2, 4, 5, 8, 9, 12, 13, 16, 17}, o) 45 | 46 | // Realistic src sample rate > dst sample rate 47 | i = []int{} 48 | for idx := 0; idx < 4*44100; idx++ { 49 | i = append(i, idx+1) 50 | } 51 | o = []int{} 52 | c = NewSampleRateConverter(44100, 16000, 2, sampleFunc) 53 | for _, s := range i { 54 | c.Add(s) 55 | } 56 | assert.Len(t, o, 4*16000) 57 | 58 | // Create input 59 | i = []int{} 60 | for idx := 0; idx < 10; idx++ { 61 | i = append(i, idx+1) 62 | } 63 | 64 | // Simple src sample rate < dst sample rate 65 | o = []int{} 66 | c = NewSampleRateConverter(3, 5, 1, sampleFunc) 67 | for _, s := range i { 68 | c.Add(s) 69 | } 70 | assert.Equal(t, []int{1, 1, 2, 2, 3, 4, 4, 5, 5, 6, 7, 7, 8, 8, 9, 10, 10}, o) 71 | 72 | // Multi channels 73 | o = []int{} 74 | c = NewSampleRateConverter(3, 5, 2, sampleFunc) 75 | for _, s := range i { 76 | c.Add(s) 77 | } 78 | assert.Equal(t, []int{1, 2, 1, 2, 3, 4, 3, 4, 5, 6, 7, 8, 7, 8, 9, 10, 9, 10}, o) 79 | } 80 | -------------------------------------------------------------------------------- /pcm/silence.go: -------------------------------------------------------------------------------- 1 | package astipcm 2 | 3 | import ( 4 | "math" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | // SilenceDetector represents a silence detector 10 | type SilenceDetector struct { 11 | analyses []analysis 12 | buf []int 13 | m *sync.Mutex // Locks buf 14 | minAnalysesPerSilence int 15 | o SilenceDetectorOptions 16 | samplesPerAnalysis int 17 | } 18 | 19 | type analysis struct { 20 | level float64 21 | samples []int 22 | } 23 | 24 | // SilenceDetectorOptions represents a silence detector options 25 | type SilenceDetectorOptions struct { 26 | MaxSilenceAudioLevel float64 `toml:"max_silence_audio_level"` 27 | MinSilenceDuration time.Duration `toml:"min_silence_duration"` 28 | SampleRate int `toml:"sample_rate"` 29 | StepDuration time.Duration `toml:"step_duration"` 30 | } 31 | 32 | // NewSilenceDetector creates a new silence detector 33 | func NewSilenceDetector(o SilenceDetectorOptions) (d *SilenceDetector) { 34 | // Create 35 | d = &SilenceDetector{ 36 | m: &sync.Mutex{}, 37 | o: o, 38 | } 39 | 40 | // Reset 41 | d.Reset() 42 | 43 | // Default option values 44 | if d.o.MinSilenceDuration == 0 { 45 | d.o.MinSilenceDuration = time.Second 46 | } 47 | if d.o.StepDuration == 0 { 48 | d.o.StepDuration = 30 * time.Millisecond 49 | } 50 | 51 | // Compute attributes depending on options 52 | d.samplesPerAnalysis = int(math.Floor(float64(d.o.SampleRate) * d.o.StepDuration.Seconds())) 53 | d.minAnalysesPerSilence = int(math.Floor(d.o.MinSilenceDuration.Seconds() / d.o.StepDuration.Seconds())) 54 | return 55 | } 56 | 57 | // Reset resets the silence detector 58 | func (d *SilenceDetector) Reset() { 59 | // Lock 60 | d.m.Lock() 61 | defer d.m.Unlock() 62 | 63 | // Reset 64 | d.analyses = []analysis{} 65 | d.buf = []int{} 66 | } 67 | 68 | // Add adds samples to the buffer and checks whether there are valid samples between silences 69 | func (d *SilenceDetector) Add(samples []int) (validSamples [][]int) { 70 | // Lock 71 | d.m.Lock() 72 | defer d.m.Unlock() 73 | 74 | // Append samples to buffer 75 | d.buf = append(d.buf, samples...) 76 | 77 | // Analyze samples by step 78 | for len(d.buf) >= d.samplesPerAnalysis { 79 | // Append analysis 80 | d.analyses = append(d.analyses, analysis{ 81 | level: AudioLevel(d.buf[:d.samplesPerAnalysis]), 82 | samples: append([]int(nil), d.buf[:d.samplesPerAnalysis]...), 83 | }) 84 | 85 | // Remove samples from buffer 86 | d.buf = d.buf[d.samplesPerAnalysis:] 87 | } 88 | 89 | // Loop through analyses 90 | var leadingSilence, inBetween, trailingSilence int 91 | for i := 0; i < len(d.analyses); i++ { 92 | if d.analyses[i].level < d.o.MaxSilenceAudioLevel { 93 | // This is a silence 94 | 95 | // This is a leading silence 96 | if inBetween == 0 { 97 | leadingSilence++ 98 | 99 | // The leading silence is valid 100 | // We can trim its useless part 101 | if leadingSilence > d.minAnalysesPerSilence { 102 | d.analyses = d.analyses[leadingSilence-d.minAnalysesPerSilence:] 103 | i -= leadingSilence - d.minAnalysesPerSilence 104 | leadingSilence = d.minAnalysesPerSilence 105 | } 106 | continue 107 | } 108 | 109 | // This is a trailing silence 110 | trailingSilence++ 111 | 112 | // Trailing silence is invalid 113 | if trailingSilence < d.minAnalysesPerSilence { 114 | continue 115 | } 116 | 117 | // Trailing silence is valid 118 | // Loop through analyses 119 | var ss []int 120 | for _, a := range d.analyses[:i+1] { 121 | ss = append(ss, a.samples...) 122 | } 123 | 124 | // Append valid samples 125 | validSamples = append(validSamples, ss) 126 | 127 | // Remove leading silence and non silence 128 | d.analyses = d.analyses[leadingSilence+inBetween:] 129 | i -= leadingSilence + inBetween 130 | 131 | // Reset counts 132 | leadingSilence, inBetween, trailingSilence = trailingSilence, 0, 0 133 | } else { 134 | // This is not a silence 135 | 136 | // This is a leading non silence 137 | // We need to remove it 138 | if i == 0 { 139 | d.analyses = d.analyses[1:] 140 | i = -1 141 | continue 142 | } 143 | 144 | // This is the first in-between 145 | if inBetween == 0 { 146 | // The leading silence is invalid 147 | // We need to remove it as well as this first non silence 148 | if leadingSilence < d.minAnalysesPerSilence { 149 | d.analyses = d.analyses[i+1:] 150 | i = -1 151 | continue 152 | } 153 | } 154 | 155 | // This non-silence was preceded by a silence not big enough to be a valid trailing silence 156 | // We incorporate it in the in-between 157 | if trailingSilence > 0 { 158 | inBetween += trailingSilence 159 | trailingSilence = 0 160 | } 161 | 162 | // This is an in-between 163 | inBetween++ 164 | continue 165 | } 166 | } 167 | return 168 | } 169 | -------------------------------------------------------------------------------- /pcm/silence_test.go: -------------------------------------------------------------------------------- 1 | package astipcm 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestSilenceDetector(t *testing.T) { 11 | // Create silence detector 12 | sd := NewSilenceDetector(SilenceDetectorOptions{ 13 | MaxSilenceAudioLevel: 2, 14 | MinSilenceDuration: 400 * time.Millisecond, // 2 samples 15 | SampleRate: 5, 16 | StepDuration: 200 * time.Millisecond, // 1 sample 17 | }) 18 | 19 | // Leading non silences + invalid leading silence + trailing silence is leftover 20 | vs := sd.Add([]int{3, 1, 3, 1}) 21 | assert.Equal(t, [][]int(nil), vs) 22 | assert.Len(t, sd.analyses, 1) 23 | 24 | // Valid leading silence but trailing silence is insufficient for now 25 | vs = sd.Add([]int{1, 3, 3, 1}) 26 | assert.Equal(t, [][]int(nil), vs) 27 | assert.Len(t, sd.analyses, 5) 28 | 29 | // Valid samples 30 | vs = sd.Add([]int{1}) 31 | assert.Equal(t, [][]int{{1, 1, 3, 3, 1, 1}}, vs) 32 | assert.Len(t, sd.analyses, 2) 33 | 34 | // Multiple valid samples + truncate leading and trailing silences 35 | vs = sd.Add([]int{1, 1, 1, 1, 3, 3, 1, 1, 1, 1, 3, 3, 1, 1, 1, 1}) 36 | assert.Equal(t, [][]int{{1, 1, 3, 3, 1, 1}, {1, 1, 3, 3, 1, 1}}, vs) 37 | assert.Len(t, sd.analyses, 2) 38 | 39 | // Invalid in-between silences that should be kept 40 | vs = sd.Add([]int{1, 1, 1, 3, 3, 1, 3, 3, 1, 3, 3, 1, 1, 1}) 41 | assert.Equal(t, [][]int{{1, 1, 3, 3, 1, 3, 3, 1, 3, 3, 1, 1}}, vs) 42 | assert.Len(t, sd.analyses, 2) 43 | } 44 | -------------------------------------------------------------------------------- /ptr/astiptr.go: -------------------------------------------------------------------------------- 1 | package astiptr 2 | 3 | import "time" 4 | 5 | // Bool transforms a bool into a *bool 6 | func Bool(i bool) *bool { 7 | return &i 8 | } 9 | 10 | // Byte transforms a byte into a *byte 11 | func Byte(i byte) *byte { 12 | return &i 13 | } 14 | 15 | // Duration transforms a time.Duration into a *time.Duration 16 | func Duration(i time.Duration) *time.Duration { 17 | return &i 18 | } 19 | 20 | // Float transforms a float64 into a *float64 21 | func Float(i float64) *float64 { 22 | return &i 23 | } 24 | 25 | // Int transforms an int into an *int 26 | func Int(i int) *int { 27 | return &i 28 | } 29 | 30 | // Int64 transforms an int64 into an *int64 31 | func Int64(i int64) *int64 { 32 | return &i 33 | } 34 | 35 | // Str transforms a string into a *string 36 | func Str(i string) *string { 37 | return &i 38 | } 39 | 40 | // UInt8 transforms a uint8 into a *uint8 41 | func UInt8(i uint8) *uint8 { 42 | return &i 43 | } 44 | 45 | // UInt32 transforms a uint32 into a *uint32 46 | func UInt32(i uint32) *uint32 { 47 | return &i 48 | } 49 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | > I first created this repo to centralize all helpers that could be reused in other projects. 2 | > But as time went by, it started using more and more external dependencies, which is not something I want to be doing. 3 | > That's why I've decided to freeze this repo so that all projects using it still work and I've created [go-astikit](https://github.com/asticode/go-astikit) whom primary goal is to provide helpers using no dependencies. 4 | > Use it instead of this repo that won't be maintained anymore. 5 | 6 | # Astitools 7 | 8 | Astitools is a set of augmented functions for the GO programming language (http://golang.org). 9 | -------------------------------------------------------------------------------- /regexp/replace.go: -------------------------------------------------------------------------------- 1 | package astiregexp 2 | 3 | import "regexp" 4 | 5 | // ReplaceAll replaces all matches from a source 6 | func ReplaceAll(rgx *regexp.Regexp, src *[]byte, rpl []byte) { 7 | // Find all matches 8 | var start, end, delta, offset, i int 9 | var l = len(rpl) 10 | for _, indexes := range rgx.FindAllIndex(*src, -1) { 11 | // Update indexes 12 | start = indexes[0] + offset 13 | end = indexes[1] + offset 14 | delta = (end - start) - l 15 | offset -= delta 16 | 17 | // Update src length 18 | if delta < 0 { 19 | // Insert 20 | (*src) = append((*src)[:start], append(make([]byte, -delta), (*src)[start:]...)...) 21 | } else if delta > 0 { 22 | // Delete 23 | (*src) = append((*src)[:start], (*src)[start+delta:]...) 24 | } 25 | 26 | // Update src content 27 | for i = 0; i < l; i++ { 28 | (*src)[i+start] = rpl[i] 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /regexp/replace_test.go: -------------------------------------------------------------------------------- 1 | package astiregexp_test 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "testing" 7 | 8 | "github.com/asticode/go-astitools/regexp" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestReplaceAll(t *testing.T) { 13 | // Initialize 14 | rgx := regexp.MustCompile("{[A-Za-z0-9_]+}") 15 | src := []byte("/test/{m1}/test/{ma2}/test/{match3}") 16 | rpl := []byte("valuevaluevaluevaluevaluevaluevalue") 17 | 18 | // Replace all 19 | astiregexp.ReplaceAll(rgx, &src, rpl) 20 | 21 | // Assert 22 | assert.Equal(t, fmt.Sprintf("/test/%s/test/%s/test/%s", string(rpl), string(rpl), string(rpl)), string(src)) 23 | } 24 | -------------------------------------------------------------------------------- /slice/slice.go: -------------------------------------------------------------------------------- 1 | package astislice 2 | 3 | // InStringSlice checks whether a string is in a string slice 4 | func InStringSlice(i string, s []string) (found bool) { 5 | for _, v := range s { 6 | if v == i { 7 | return true 8 | } 9 | } 10 | return 11 | } 12 | -------------------------------------------------------------------------------- /slice/slice_test.go: -------------------------------------------------------------------------------- 1 | package astislice_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/asticode/go-astitools/slice" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestInStringSlice(t *testing.T) { 11 | assert.False(t, astislice.InStringSlice("test", []string{"test1", "test2"})) 12 | assert.True(t, astislice.InStringSlice("test1", []string{"test1", "test2"})) 13 | } 14 | -------------------------------------------------------------------------------- /sort/int64.go: -------------------------------------------------------------------------------- 1 | package astisort 2 | 3 | import "sort" 4 | 5 | // Int64 sorts a slice of int64s in increasing order. 6 | func Int64(a []int64) { sort.Sort(Int64Slice(a)) } 7 | 8 | // Int64Slice attaches the methods of Interface to []int64, sorting in increasing order. 9 | type Int64Slice []int64 10 | 11 | func (p Int64Slice) Len() int { return len(p) } 12 | func (p Int64Slice) Less(i, j int) bool { return p[i] < p[j] } 13 | func (p Int64Slice) Swap(i, j int) { p[i], p[j] = p[j], p[i] } 14 | -------------------------------------------------------------------------------- /sort/uint16.go: -------------------------------------------------------------------------------- 1 | package astisort 2 | 3 | import "sort" 4 | 5 | // Uint16 sorts a slice of uint16s in increasing order. 6 | func Uint16(a []uint16) { sort.Sort(UInt16Slice(a)) } 7 | 8 | // UInt16Slice attaches the methods of Interface to []uint16, sorting in increasing order. 9 | type UInt16Slice []uint16 10 | 11 | func (p UInt16Slice) Len() int { return len(p) } 12 | func (p UInt16Slice) Less(i, j int) bool { return p[i] < p[j] } 13 | func (p UInt16Slice) Swap(i, j int) { p[i], p[j] = p[j], p[i] } 14 | -------------------------------------------------------------------------------- /sort/uint8.go: -------------------------------------------------------------------------------- 1 | package astisort 2 | 3 | import "sort" 4 | 5 | // Uint8 sorts a slice of uint8s in increasing order. 6 | func Uint8(a []uint8) { sort.Sort(UInt8Slice(a)) } 7 | 8 | // UInt8Slice attaches the methods of Interface to []uint8, sorting in increasing order. 9 | type UInt8Slice []uint8 10 | 11 | func (p UInt8Slice) Len() int { return len(p) } 12 | func (p UInt8Slice) Less(i, j int) bool { return p[i] < p[j] } 13 | func (p UInt8Slice) Swap(i, j int) { p[i], p[j] = p[j], p[i] } 14 | -------------------------------------------------------------------------------- /ssh/auth.go: -------------------------------------------------------------------------------- 1 | package astissh 2 | 3 | import ( 4 | "io/ioutil" 5 | 6 | "github.com/pkg/errors" 7 | "golang.org/x/crypto/ssh" 8 | ) 9 | 10 | // AuthMethodPublicKey creates a public key auth method 11 | func AuthMethodPublicKey(paths ...string) (m ssh.AuthMethod, err error) { 12 | // Loop through paths 13 | var ss []ssh.Signer 14 | for _, p := range paths { 15 | // Read private key 16 | var b []byte 17 | if b, err = ioutil.ReadFile(p); err != nil { 18 | err = errors.Wrapf(err, "main: reading private key %s failed", p) 19 | return 20 | } 21 | 22 | // Parse private key 23 | var s ssh.Signer 24 | if s, err = ssh.ParsePrivateKey(b); err != nil { 25 | err = errors.Wrapf(err, "main: parsing private key %s failed", p) 26 | return 27 | } 28 | 29 | // Append 30 | ss = append(ss, s) 31 | } 32 | 33 | // Create auth method 34 | m = ssh.PublicKeys(ss...) 35 | return 36 | } 37 | -------------------------------------------------------------------------------- /ssh/copy.go: -------------------------------------------------------------------------------- 1 | package astissh 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | 10 | "fmt" 11 | 12 | "github.com/asticode/go-astilog" 13 | "github.com/asticode/go-astitools/defer" 14 | "github.com/asticode/go-astitools/io" 15 | "github.com/pkg/errors" 16 | "golang.org/x/crypto/ssh" 17 | ) 18 | 19 | // SessionFunc represents a function that can create a new ssh session 20 | type SessionFunc func() (*ssh.Session, *astidefer.Closer, error) 21 | 22 | // Copy is a cancellable copy 23 | // If src is a file, dst must be the full path to file once copied 24 | // If src is a dir, dst must be the full path to the dir once copied 25 | func Copy(ctx context.Context, src, dst string, fn SessionFunc) (err error) { 26 | // Check context 27 | if err = ctx.Err(); err != nil { 28 | return 29 | } 30 | 31 | // Stat src 32 | var statSrc os.FileInfo 33 | if statSrc, err = os.Stat(src); err != nil { 34 | err = errors.Wrapf(err, "stating %s failed", src) 35 | return 36 | } 37 | 38 | // Dir 39 | if statSrc.IsDir() { 40 | if err = filepath.Walk(src, func(path string, info os.FileInfo, errWalk error) (err error) { 41 | // Check error 42 | if errWalk != nil { 43 | err = errWalk 44 | return 45 | } 46 | 47 | // Do not process root 48 | if src == path { 49 | return 50 | } 51 | 52 | // Copy 53 | var p = filepath.Join(dst, strings.TrimPrefix(path, filepath.Clean(src))) 54 | if err = Copy(ctx, path, p, fn); err != nil { 55 | err = errors.Wrapf(err, "copying %s to %s failed", path, p) 56 | return 57 | 58 | } 59 | return 60 | }); err != nil { 61 | return 62 | } 63 | return 64 | } 65 | 66 | // Create ssh session 67 | var s *ssh.Session 68 | var c *astidefer.Closer 69 | if s, c, err = fn(); err != nil { 70 | err = errors.Wrap(err, "main: creating ssh session failed") 71 | return 72 | } 73 | defer c.Close() 74 | 75 | // Create the destination folder 76 | if err = s.Run("mkdir -p " + filepath.Dir(dst)); err != nil { 77 | err = errors.Wrapf(err, "astissh: creating %s failed", filepath.Dir(dst)) 78 | return 79 | } 80 | 81 | // Open file 82 | var f *os.File 83 | if f, err = os.Open(src); err != nil { 84 | err = errors.Wrapf(err, "astissh: opening %s failed", src) 85 | return 86 | } 87 | defer f.Close() 88 | 89 | // Create ssh session 90 | if s, c, err = fn(); err != nil { 91 | err = errors.Wrap(err, "main: creating ssh session failed") 92 | return 93 | } 94 | defer c.Close() 95 | 96 | // Create stdin pipe 97 | var stdin io.WriteCloser 98 | if stdin, err = s.StdinPipe(); err != nil { 99 | err = errors.Wrap(err, "astissh: creating stdin pipe failed") 100 | return 101 | } 102 | defer stdin.Close() 103 | 104 | // Start "scp" command 105 | astilog.Debugf("astissh: copying %s to %s", filepath.Base(dst), filepath.Dir(dst)) 106 | if err = s.Start("scp -qt " + filepath.Dir(dst)); err != nil { 107 | err = errors.Wrapf(err, "astissh: scp to %s failed", dst) 108 | return 109 | } 110 | 111 | // Send metadata 112 | if _, err = fmt.Fprintln(stdin, fmt.Sprintf("C%04o", statSrc.Mode().Perm()), statSrc.Size(), filepath.Base(dst)); err != nil { 113 | err = errors.Wrapf(err, "astissh: sending metadata failed") 114 | return 115 | } 116 | 117 | // Copy 118 | if _, err = astiio.Copy(ctx, f, stdin); err != nil { 119 | err = errors.Wrap(err, "astissh: copying failed") 120 | return 121 | } 122 | 123 | // Send close 124 | if _, err = fmt.Fprint(stdin, "\x00"); err != nil { 125 | err = errors.Wrap(err, "astissh: sending close failed") 126 | return 127 | } 128 | 129 | // Close stdin 130 | stdin.Close() 131 | 132 | // Wait 133 | if err = s.Wait(); err != nil { 134 | err = errors.Wrap(err, "astissh: waiting failed") 135 | return 136 | } 137 | return 138 | } 139 | -------------------------------------------------------------------------------- /stat/duration.go: -------------------------------------------------------------------------------- 1 | package astistat 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | // DurationRatioStat is an object capable of computing a duration ratio stat properly 9 | type DurationRatioStat struct { 10 | d time.Duration 11 | isStarted bool 12 | m *sync.Mutex 13 | startedAt map[interface{}]time.Time 14 | } 15 | 16 | // NewDurationRatioStat creates a new duration ratio stat 17 | func NewDurationRatioStat() *DurationRatioStat { 18 | return &DurationRatioStat{ 19 | startedAt: make(map[interface{}]time.Time), 20 | m: &sync.Mutex{}, 21 | } 22 | } 23 | 24 | // Add starts recording a new duration 25 | func (s *DurationRatioStat) Add(k interface{}) { 26 | s.m.Lock() 27 | defer s.m.Unlock() 28 | if !s.isStarted { 29 | return 30 | } 31 | s.startedAt[k] = time.Now() 32 | } 33 | 34 | // Done indicates the duration is now done 35 | func (s *DurationRatioStat) Done(k interface{}) { 36 | s.m.Lock() 37 | defer s.m.Unlock() 38 | if !s.isStarted { 39 | return 40 | } 41 | s.d += time.Now().Sub(s.startedAt[k]) 42 | delete(s.startedAt, k) 43 | } 44 | 45 | // Value implements the StatHandler interface 46 | func (s *DurationRatioStat) Value(delta time.Duration) (o interface{}) { 47 | // Lock 48 | s.m.Lock() 49 | defer s.m.Unlock() 50 | 51 | // Get current values 52 | n := time.Now() 53 | d := s.d 54 | 55 | // Loop through waits still not finished 56 | for k, v := range s.startedAt { 57 | d += n.Sub(v) 58 | s.startedAt[k] = n 59 | } 60 | 61 | // Compute stat 62 | o = float64(d) / float64(delta) * 100 63 | s.d = 0 64 | return 65 | } 66 | 67 | // Start implements the StatHandler interface 68 | func (s *DurationRatioStat) Start() { 69 | s.m.Lock() 70 | defer s.m.Unlock() 71 | s.d = 0 72 | s.isStarted = true 73 | s.startedAt = make(map[interface{}]time.Time) 74 | } 75 | 76 | // Stop implements the StatHandler interface 77 | func (s *DurationRatioStat) Stop() { 78 | s.m.Lock() 79 | defer s.m.Unlock() 80 | s.isStarted = false 81 | } 82 | -------------------------------------------------------------------------------- /stat/increment.go: -------------------------------------------------------------------------------- 1 | package astistat 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | // IncrementStat is an object capable of computing an increment stat properly 9 | type IncrementStat struct { 10 | c int64 11 | isStarted bool 12 | m *sync.Mutex 13 | } 14 | 15 | // NewIncrementStat creates a new increment stat 16 | func NewIncrementStat() *IncrementStat { 17 | return &IncrementStat{m: &sync.Mutex{}} 18 | } 19 | 20 | // Add increments the stat 21 | func (s *IncrementStat) Add(delta int64) { 22 | s.m.Lock() 23 | defer s.m.Unlock() 24 | if !s.isStarted { 25 | return 26 | } 27 | s.c += delta 28 | } 29 | 30 | // Start implements the StatHandler interface 31 | func (s *IncrementStat) Start() { 32 | s.m.Lock() 33 | defer s.m.Unlock() 34 | s.c = 0 35 | s.isStarted = true 36 | } 37 | 38 | // Stop implements the StatHandler interface 39 | func (s *IncrementStat) Stop() { 40 | s.m.Lock() 41 | defer s.m.Unlock() 42 | s.isStarted = true 43 | } 44 | 45 | // Value implements the StatHandler interface 46 | func (s *IncrementStat) Value(delta time.Duration) interface{} { 47 | s.m.Lock() 48 | defer s.m.Unlock() 49 | c := s.c 50 | s.c = 0 51 | return float64(c) / delta.Seconds() 52 | } 53 | -------------------------------------------------------------------------------- /stat/stater.go: -------------------------------------------------------------------------------- 1 | package astistat 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | // Stater is an object that can compute and handle stats 10 | type Stater struct { 11 | cancel context.CancelFunc 12 | ctx context.Context 13 | fn StatsHandleFunc 14 | oStart *sync.Once 15 | oStop *sync.Once 16 | period time.Duration 17 | ss []stat 18 | } 19 | 20 | // Stat represents a stat 21 | type Stat struct { 22 | StatMetadata 23 | Value interface{} 24 | } 25 | 26 | // StatsHandleFunc is a method that can handle stats 27 | type StatsHandleFunc func(stats []Stat) 28 | 29 | // StatMetadata represents a stat metadata 30 | type StatMetadata struct { 31 | Description string 32 | Label string 33 | Unit string 34 | } 35 | 36 | // StatHandler represents a stat handler 37 | type StatHandler interface { 38 | Start() 39 | Stop() 40 | Value(delta time.Duration) interface{} 41 | } 42 | 43 | type stat struct { 44 | h StatHandler 45 | m StatMetadata 46 | } 47 | 48 | // NewStater creates a new stater 49 | func NewStater(period time.Duration, fn StatsHandleFunc) *Stater { 50 | return &Stater{ 51 | fn: fn, 52 | oStart: &sync.Once{}, 53 | oStop: &sync.Once{}, 54 | period: period, 55 | } 56 | } 57 | 58 | // Start starts the stater 59 | func (s *Stater) Start(ctx context.Context) { 60 | // Make sure the stater can only be started once 61 | s.oStart.Do(func() { 62 | // Check context 63 | if ctx.Err() != nil { 64 | return 65 | } 66 | 67 | // Reset context 68 | s.ctx, s.cancel = context.WithCancel(ctx) 69 | 70 | // Reset once 71 | s.oStop = &sync.Once{} 72 | 73 | // Start stats 74 | for _, v := range s.ss { 75 | v.h.Start() 76 | } 77 | 78 | // Execute the rest in a go routine 79 | go func() { 80 | // Create ticker 81 | t := time.NewTicker(s.period) 82 | defer t.Stop() 83 | 84 | // Loop 85 | lastStatAt := time.Now() 86 | for { 87 | select { 88 | case <-t.C: 89 | // Get delta 90 | now := time.Now() 91 | delta := now.Sub(lastStatAt) 92 | lastStatAt = now 93 | 94 | // Loop through stats 95 | var stats []Stat 96 | for _, v := range s.ss { 97 | stats = append(stats, Stat{ 98 | StatMetadata: v.m, 99 | Value: v.h.Value(delta), 100 | }) 101 | } 102 | 103 | // Handle stats 104 | go s.fn(stats) 105 | case <-s.ctx.Done(): 106 | // Stop stats 107 | for _, v := range s.ss { 108 | v.h.Stop() 109 | } 110 | return 111 | } 112 | } 113 | }() 114 | }) 115 | } 116 | 117 | // AddStat adds a stat 118 | func (s *Stater) AddStat(m StatMetadata, h StatHandler) { 119 | s.ss = append(s.ss, stat{ 120 | h: h, 121 | m: m, 122 | }) 123 | } 124 | 125 | // Stop stops the stater 126 | func (s *Stater) Stop() { 127 | // Make sure the stater can only be stopped once 128 | s.oStop.Do(func() { 129 | // Cancel context 130 | if s.cancel != nil { 131 | s.cancel() 132 | } 133 | 134 | // Reset once 135 | s.oStart = &sync.Once{} 136 | }) 137 | } 138 | 139 | // StatsMetadata returns the stats metadata 140 | func (s *Stater) StatsMetadata() (ms []StatMetadata) { 141 | ms = []StatMetadata{} 142 | for _, v := range s.ss { 143 | ms = append(ms, v.m) 144 | } 145 | return 146 | } 147 | 148 | // StatHandlerWithoutStart represents a stat handler that doesn't have to start or stop 149 | type StatHandlerWithoutStart func(delta time.Duration) interface{} 150 | 151 | // Start implements the StatHandler interface 152 | func (h StatHandlerWithoutStart) Start() {} 153 | 154 | // Stop implements the StatHandler interface 155 | func (h StatHandlerWithoutStart) Stop() {} 156 | 157 | // Value implements the StatHandler interface 158 | func (h StatHandlerWithoutStart) Value(delta time.Duration) interface{} { return h(delta) } 159 | -------------------------------------------------------------------------------- /string/length.go: -------------------------------------------------------------------------------- 1 | package astistring 2 | 3 | // ToLength forces the length of a string 4 | func ToLength(i, rpl string, length int) string { 5 | if len(i) == length { 6 | return i 7 | } else if len(i) > length { 8 | return i[:length] 9 | } else { 10 | for idx := 0; idx <= length-len(i); idx++ { 11 | i += rpl 12 | } 13 | return i 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /string/length_test.go: -------------------------------------------------------------------------------- 1 | package astistring_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/asticode/go-astitools/string" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestToLength(t *testing.T) { 11 | assert.Equal(t, "test", astistring.ToLength("test", " ", 4)) 12 | assert.Equal(t, "test", astistring.ToLength("testtest", " ", 4)) 13 | assert.Equal(t, "test ", astistring.ToLength("test", " ", 6)) 14 | } 15 | -------------------------------------------------------------------------------- /string/rand.go: -------------------------------------------------------------------------------- 1 | package astistring 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | ) 7 | 8 | const ( 9 | letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890" 10 | letterIdxBits = 6 // 6 bits to represent a letter index 11 | letterIdxMask = 1<= 0; { 23 | if remain == 0 { 24 | cache, remain = src.Int63(), letterIdxMax 25 | } 26 | if idx := int(cache & letterIdxMask); idx < len(letterBytes) { 27 | b[i] = letterBytes[idx] 28 | i-- 29 | } 30 | cache >>= letterIdxBits 31 | remain-- 32 | } 33 | 34 | return string(b) 35 | } 36 | -------------------------------------------------------------------------------- /sync/chan.go: -------------------------------------------------------------------------------- 1 | package astisync 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | 7 | astiworker "github.com/asticode/go-astitools/worker" 8 | ) 9 | 10 | // Orders 11 | const ( 12 | FIFOOrder = "fifo" 13 | FILOOrder = "filo" 14 | ) 15 | 16 | // Chan is an object capable of doing stuff in a specific order without blocking when adding new items 17 | // in the @ 18 | type Chan struct { 19 | cancel context.CancelFunc 20 | c *sync.Cond 21 | ctx context.Context 22 | fs []func() 23 | mc *sync.Mutex // Locks ctx 24 | mf *sync.Mutex // Locks fs 25 | o ChanOptions 26 | oStart *sync.Once 27 | oStop *sync.Once 28 | } 29 | 30 | // ChanOptions are Chan options 31 | type ChanOptions struct { 32 | Order string 33 | // By default the funcs not yet processed when the context is cancelled will be dropped. However if TaskFunc is not 34 | // nil all funcs will be processed even after the context has been cancelled. 35 | TaskFunc astiworker.TaskFunc 36 | } 37 | 38 | // NewChan creates a new Chan 39 | func NewChan(o ChanOptions) *Chan { 40 | return &Chan{ 41 | c: sync.NewCond(&sync.Mutex{}), 42 | mc: &sync.Mutex{}, 43 | mf: &sync.Mutex{}, 44 | o: o, 45 | oStart: &sync.Once{}, 46 | oStop: &sync.Once{}, 47 | } 48 | } 49 | 50 | // Start starts the chan by looping through functions in the buffer and executing them if any, or waiting for a new one 51 | // otherwise 52 | func (c *Chan) Start(ctx context.Context) { 53 | // Make sure to start only once 54 | c.oStart.Do(func() { 55 | // Create context 56 | c.mc.Lock() 57 | c.ctx, c.cancel = context.WithCancel(ctx) 58 | c.mc.Unlock() 59 | 60 | // Reset once 61 | c.oStop = &sync.Once{} 62 | 63 | // Handle context 64 | go func() { 65 | // Wait for context to be done 66 | <-c.ctx.Done() 67 | 68 | // Signal 69 | c.c.L.Lock() 70 | c.c.Signal() 71 | c.c.L.Unlock() 72 | }() 73 | 74 | // Loop 75 | for { 76 | // Lock cond here in case a func is added between retrieving l and doing the if on it 77 | c.c.L.Lock() 78 | 79 | // Get number of funcs in buffer 80 | c.mf.Lock() 81 | l := len(c.fs) 82 | c.mf.Unlock() 83 | 84 | // Only return if context has been cancelled and: 85 | // - the user wants to drop funcs that has not yet been processed 86 | // - the buffer is empty otherwise 87 | c.mc.Lock() 88 | if c.ctx.Err() != nil && (c.o.TaskFunc == nil || l == 0) { 89 | c.mc.Unlock() 90 | c.c.L.Unlock() 91 | return 92 | } 93 | c.mc.Unlock() 94 | 95 | // No funcs in buffer 96 | if l == 0 { 97 | c.c.Wait() 98 | c.c.L.Unlock() 99 | continue 100 | } 101 | c.c.L.Unlock() 102 | 103 | // Get first func 104 | c.mf.Lock() 105 | fn := c.fs[0] 106 | c.mf.Unlock() 107 | 108 | // Execute func 109 | fn() 110 | 111 | // Remove first func 112 | c.mf.Lock() 113 | c.fs = c.fs[1:] 114 | c.mf.Unlock() 115 | } 116 | }) 117 | } 118 | 119 | // Stop stops the chan 120 | func (c *Chan) Stop() { 121 | // Make sure to stop only once 122 | c.oStop.Do(func() { 123 | // Cancel context 124 | if c.cancel != nil { 125 | c.cancel() 126 | } 127 | 128 | // Reset once 129 | c.oStart = &sync.Once{} 130 | }) 131 | } 132 | 133 | // Add adds a new item to the chan 134 | func (c *Chan) Add(fn func()) { 135 | // Check context 136 | c.mc.Lock() 137 | if c.ctx != nil && c.ctx.Err() != nil { 138 | c.mc.Unlock() 139 | return 140 | } 141 | c.mc.Unlock() 142 | 143 | // Wrap func 144 | wfn := fn 145 | if c.o.TaskFunc != nil { 146 | // Create task 147 | t := c.o.TaskFunc() 148 | 149 | // Wrap function 150 | wfn = func() { 151 | // Task is done 152 | defer t.Done() 153 | 154 | // Custom 155 | fn() 156 | } 157 | } 158 | 159 | // Add func to buffer 160 | c.mf.Lock() 161 | if c.o.Order == FILOOrder { 162 | c.fs = append([]func(){wfn}, c.fs...) 163 | } else { 164 | c.fs = append(c.fs, wfn) 165 | } 166 | c.mf.Unlock() 167 | 168 | // Signal 169 | c.c.L.Lock() 170 | c.c.Signal() 171 | c.c.L.Unlock() 172 | } 173 | 174 | // Reset resets the chan 175 | func (c *Chan) Reset() { 176 | c.mf.Lock() 177 | defer c.mf.Unlock() 178 | c.fs = []func(){} 179 | } 180 | -------------------------------------------------------------------------------- /sync/ctx_queue.go: -------------------------------------------------------------------------------- 1 | package astisync 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | "sync/atomic" 7 | 8 | "github.com/asticode/go-astitools/stat" 9 | ) 10 | 11 | // CtxQueue is a queue that can 12 | // - handle a context without dropping any messages sent before the context is cancelled 13 | // - ensure that sending a message is not blocking if 14 | // - the queue has not been started 15 | // - the context has been cancelled 16 | type CtxQueue struct { 17 | c chan ctxQueueMessage 18 | ctxIsDone uint32 19 | hasStarted uint32 20 | o *sync.Once 21 | startC *sync.Cond 22 | statListen *astistat.DurationRatioStat 23 | } 24 | 25 | type ctxQueueMessage struct { 26 | c *sync.Cond 27 | ctxIsDone bool 28 | p interface{} 29 | } 30 | 31 | // NewCtxQueue creates a new ctx queue 32 | func NewCtxQueue() *CtxQueue { 33 | return &CtxQueue{ 34 | c: make(chan ctxQueueMessage), 35 | o: &sync.Once{}, 36 | startC: sync.NewCond(&sync.Mutex{}), 37 | statListen: astistat.NewDurationRatioStat(), 38 | } 39 | } 40 | 41 | // HandleCtx handles the ctx 42 | func (q *CtxQueue) HandleCtx(ctx context.Context) { 43 | // Wait for ctx to be done 44 | <-ctx.Done() 45 | 46 | // Broadcast 47 | q.startC.L.Lock() 48 | atomic.StoreUint32(&q.ctxIsDone, 1) 49 | q.startC.Broadcast() 50 | q.startC.L.Unlock() 51 | 52 | // If the queue has started, send the ctx message 53 | if d := atomic.LoadUint32(&q.hasStarted); d == 1 { 54 | q.c <- ctxQueueMessage{ctxIsDone: true} 55 | } 56 | } 57 | 58 | // Start starts the queue 59 | func (q *CtxQueue) Start(fn func(p interface{})) { 60 | // Make sure the queue can only be started once 61 | q.o.Do(func() { 62 | // Reset ctx 63 | atomic.StoreUint32(&q.ctxIsDone, 0) 64 | 65 | // Broadcast 66 | q.startC.L.Lock() 67 | q.startC.Broadcast() 68 | atomic.StoreUint32(&q.hasStarted, 1) 69 | q.startC.L.Unlock() 70 | 71 | // Wait is starting 72 | q.statListen.Add(true) 73 | 74 | // Loop 75 | for { 76 | select { 77 | case m := <-q.c: 78 | // Wait is done 79 | q.statListen.Done(true) 80 | 81 | // Check context 82 | if m.ctxIsDone { 83 | return 84 | } 85 | 86 | // Handle payload 87 | fn(m.p) 88 | 89 | // Broadcast the fact that the process is done 90 | m.c.L.Lock() 91 | m.c.Broadcast() 92 | m.c.L.Unlock() 93 | 94 | // Wait is starting 95 | q.statListen.Add(true) 96 | } 97 | } 98 | }) 99 | } 100 | 101 | // Send sends a message in the queue and blocks until the message has been fully processed 102 | // Block indicates whether to block until the message has been fully processed 103 | func (q *CtxQueue) Send(p interface{}) { 104 | // Make sure to lock here 105 | q.startC.L.Lock() 106 | 107 | // Context is done 108 | if d := atomic.LoadUint32(&q.ctxIsDone); d == 1 { 109 | q.startC.L.Unlock() 110 | return 111 | } 112 | 113 | // Check whether queue has been started 114 | if d := atomic.LoadUint32(&q.hasStarted); d == 0 { 115 | // We either wait for the queue to start or for the ctx to be done 116 | q.startC.Wait() 117 | 118 | // Context is done 119 | if d := atomic.LoadUint32(&q.ctxIsDone); d == 1 { 120 | q.startC.L.Unlock() 121 | return 122 | } 123 | } 124 | q.startC.L.Unlock() 125 | 126 | // Create cond 127 | c := sync.NewCond(&sync.Mutex{}) 128 | c.L.Lock() 129 | 130 | // Send message 131 | q.c <- ctxQueueMessage{ 132 | c: c, 133 | p: p, 134 | } 135 | 136 | // Wait for handling to be done 137 | c.Wait() 138 | } 139 | 140 | // Stop stops the queue properly 141 | func (q *CtxQueue) Stop() { 142 | atomic.StoreUint32(&q.hasStarted, 0) 143 | q.o = &sync.Once{} 144 | } 145 | 146 | // AddStats adds queue stats 147 | func (q *CtxQueue) AddStats(s *astistat.Stater) { 148 | // Add wait stat 149 | s.AddStat(astistat.StatMetadata{ 150 | Description: "Percentage of time spent listening and waiting for new object", 151 | Label: "Listen ratio", 152 | Unit: "%", 153 | }, q.statListen) 154 | } 155 | -------------------------------------------------------------------------------- /sync/mutex.go: -------------------------------------------------------------------------------- 1 | package astisync 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | "sync" 7 | "time" 8 | 9 | "github.com/asticode/go-astilog" 10 | ) 11 | 12 | // RWMutex represents a RWMutex capable of logging its actions to ease deadlock debugging 13 | type RWMutex struct { 14 | lastSuccessfulLockCaller string 15 | log bool 16 | mutex *sync.RWMutex 17 | name string 18 | } 19 | 20 | // NewRWMutex creates a new RWMutex 21 | func NewRWMutex(name string, log bool) *RWMutex { 22 | return &RWMutex{ 23 | log: log, 24 | mutex: &sync.RWMutex{}, 25 | name: name, 26 | } 27 | } 28 | 29 | // Lock write locks the mutex 30 | func (m *RWMutex) Lock() { 31 | var caller string 32 | if _, file, line, ok := runtime.Caller(1); ok { 33 | caller = fmt.Sprintf("%s:%d", file, line) 34 | } 35 | if m.log { 36 | astilog.Debugf("Requesting lock for %s at %s", m.name, caller) 37 | } 38 | m.mutex.Lock() 39 | if m.log { 40 | astilog.Debugf("Lock acquired for %s at %s", m.name, caller) 41 | } 42 | m.lastSuccessfulLockCaller = caller 43 | } 44 | 45 | // Unlock write unlocks the mutex 46 | func (m *RWMutex) Unlock() { 47 | m.mutex.Unlock() 48 | if m.log { 49 | astilog.Debugf("Unlock executed for %s", m.name) 50 | } 51 | } 52 | 53 | // RLock read locks the mutex 54 | func (m *RWMutex) RLock() { 55 | var caller string 56 | if _, file, line, ok := runtime.Caller(1); ok { 57 | caller = fmt.Sprintf("%s:%d", file, line) 58 | } 59 | if m.log { 60 | astilog.Debugf("Requesting rlock for %s at %s", m.name, caller) 61 | } 62 | m.mutex.RLock() 63 | if m.log { 64 | astilog.Debugf("RLock acquired for %s at %s", m.name, caller) 65 | } 66 | m.lastSuccessfulLockCaller = caller 67 | } 68 | 69 | // RUnlock read unlocks the mutex 70 | func (m *RWMutex) RUnlock() { 71 | m.mutex.RUnlock() 72 | if m.log { 73 | astilog.Debugf("RUnlock executed for %s", m.name) 74 | } 75 | } 76 | 77 | // IsDeadlocked checks whether the mutex is deadlocked with a given timeout and returns the last caller 78 | func (m *RWMutex) IsDeadlocked(timeout time.Duration) (o bool, c string) { 79 | o = true 80 | c = m.lastSuccessfulLockCaller 81 | var channelLockAcquired = make(chan bool) 82 | go func() { 83 | m.mutex.Lock() 84 | defer m.mutex.Unlock() 85 | close(channelLockAcquired) 86 | }() 87 | for { 88 | select { 89 | case <-channelLockAcquired: 90 | o = false 91 | return 92 | case <-time.After(timeout): 93 | return 94 | } 95 | } 96 | return 97 | } 98 | -------------------------------------------------------------------------------- /sync/mutex_test.go: -------------------------------------------------------------------------------- 1 | package astisync_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/asticode/go-astitools/sync" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestRWMutex_IsDeadlocked(t *testing.T) { 12 | var m = astisync.NewRWMutex("test", false) 13 | d, _ := m.IsDeadlocked(time.Millisecond) 14 | assert.False(t, d) 15 | m.Lock() 16 | d, c := m.IsDeadlocked(time.Millisecond) 17 | assert.True(t, d) 18 | assert.Contains(t, c, "github.com/asticode/go-astitools/sync/mutex_test.go:15") 19 | } 20 | -------------------------------------------------------------------------------- /template/template.go: -------------------------------------------------------------------------------- 1 | package astitemplate 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | "text/template" 10 | ) 11 | 12 | // ParseDirectory parses a directory recursively 13 | func ParseDirectory(i, ext string) (t *template.Template, err error) { 14 | // Parse templates 15 | i = filepath.Clean(i) 16 | t = template.New("Root") 17 | return t, filepath.Walk(i, func(path string, info os.FileInfo, e error) (err error) { 18 | // Check input error 19 | if e != nil { 20 | err = e 21 | return 22 | } 23 | 24 | // Only process files 25 | if info.IsDir() { 26 | return 27 | } 28 | 29 | // Check extension 30 | if ext != "" && filepath.Ext(path) != ext { 31 | return 32 | } 33 | 34 | // Read file 35 | var b []byte 36 | if b, err = ioutil.ReadFile(path); err != nil { 37 | return 38 | } 39 | 40 | // Parse template 41 | var c = t.New(filepath.ToSlash(strings.TrimPrefix(path, i))) 42 | if _, err = c.Parse(string(b)); err != nil { 43 | return fmt.Errorf("%s while parsing template %s", err, path) 44 | } 45 | return 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /template/template_test.go: -------------------------------------------------------------------------------- 1 | package astitemplate_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/asticode/go-astitools/template" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestParseDirectory(t *testing.T) { 11 | tmpl, err := astitemplate.ParseDirectory("tests", "") 12 | assert.NoError(t, err) 13 | assert.Len(t, tmpl.Templates(), 4) 14 | tmpl, err = astitemplate.ParseDirectory("tests", ".html") 15 | assert.NoError(t, err) 16 | assert.Len(t, tmpl.Templates(), 3) 17 | } 18 | -------------------------------------------------------------------------------- /template/templater.go: -------------------------------------------------------------------------------- 1 | package astitemplate 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | "sync" 9 | "text/template" 10 | 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | // Templater represents an object capable of storing templates 15 | type Templater struct { 16 | layouts []string 17 | m sync.Mutex 18 | templates map[string]*template.Template 19 | } 20 | 21 | // NewTemplater creates a new templater 22 | func NewTemplater() *Templater { 23 | return &Templater{templates: make(map[string]*template.Template)} 24 | } 25 | 26 | // AddLayoutsFromDir walks through a dir and add files as layouts 27 | func (t *Templater) AddLayoutsFromDir(dirPath, ext string) (err error) { 28 | // Get layouts 29 | if err = filepath.Walk(dirPath, func(path string, info os.FileInfo, e error) (err error) { 30 | // Check input error 31 | if e != nil { 32 | err = errors.Wrapf(e, "astitemplate: walking layouts has an input error for path %s", path) 33 | return 34 | } 35 | 36 | // Only process files 37 | if info.IsDir() { 38 | return 39 | } 40 | 41 | // Check extension 42 | if ext != "" && filepath.Ext(path) != ext { 43 | return 44 | } 45 | 46 | // Read layout 47 | var b []byte 48 | if b, err = ioutil.ReadFile(path); err != nil { 49 | err = errors.Wrapf(err, "astitemplate: reading %s failed", path) 50 | return 51 | } 52 | 53 | // Add layout 54 | t.AddLayout(string(b)) 55 | return 56 | }); err != nil { 57 | err = errors.Wrapf(err, "astitemplate: walking layouts in %s failed", dirPath) 58 | return 59 | } 60 | return 61 | } 62 | 63 | // AddTemplatesFromDir walks through a dir and add files as templates 64 | func (t *Templater) AddTemplatesFromDir(dirPath, ext string) (err error) { 65 | // Loop through templates 66 | if err = filepath.Walk(dirPath, func(path string, info os.FileInfo, e error) (err error) { 67 | // Check input error 68 | if e != nil { 69 | err = errors.Wrapf(e, "astitemplate: walking templates has an input error for path %s", path) 70 | return 71 | } 72 | 73 | // Only process files 74 | if info.IsDir() { 75 | return 76 | } 77 | 78 | // Check extension 79 | if ext != "" && filepath.Ext(path) != ext { 80 | return 81 | } 82 | 83 | // Read file 84 | var b []byte 85 | if b, err = ioutil.ReadFile(path); err != nil { 86 | err = errors.Wrapf(err, "astitemplate: reading template content of %s failed", path) 87 | return 88 | } 89 | 90 | // Add template 91 | // We use ToSlash to homogenize Windows path 92 | if err = t.AddTemplate(filepath.ToSlash(strings.TrimPrefix(path, dirPath)), string(b)); err != nil { 93 | err = errors.Wrap(err, "astitemplate: adding template failed") 94 | return 95 | } 96 | return 97 | }); err != nil { 98 | err = errors.Wrapf(err, "astitemplate: walking templates in %s failed", dirPath) 99 | return 100 | } 101 | return 102 | } 103 | 104 | // AddLayout adds a new layout 105 | func (t *Templater) AddLayout(c string) { 106 | t.layouts = append(t.layouts, c) 107 | } 108 | 109 | // AddTemplate adds a new template 110 | func (t *Templater) AddTemplate(path, content string) (err error) { 111 | // Parse 112 | var tpl *template.Template 113 | if tpl, err = t.Parse(content); err != nil { 114 | err = errors.Wrapf(err, "astitemplate: parsing template for path %s failed", path) 115 | return 116 | } 117 | 118 | // Add template 119 | t.m.Lock() 120 | t.templates[path] = tpl 121 | t.m.Unlock() 122 | return 123 | } 124 | 125 | // DelTemplate deletes a template 126 | func (t *Templater) DelTemplate(path string) { 127 | t.m.Lock() 128 | defer t.m.Unlock() 129 | delete(t.templates, path) 130 | } 131 | 132 | // Template retrieves a templates 133 | func (t *Templater) Template(path string) (tpl *template.Template, ok bool) { 134 | t.m.Lock() 135 | defer t.m.Unlock() 136 | tpl, ok = t.templates[path] 137 | return 138 | } 139 | 140 | // Parse parses the content of a template 141 | func (t *Templater) Parse(content string) (o *template.Template, err error) { 142 | // Parse content 143 | o = template.New("root") 144 | if o, err = o.Parse(content); err != nil { 145 | err = errors.Wrap(err, "astitemplate: parsing template content failed") 146 | return 147 | } 148 | 149 | // Parse layouts 150 | for idx, l := range t.layouts { 151 | if o, err = o.Parse(l); err != nil { 152 | err = errors.Wrapf(err, "astitemplate: parsing layout #%d failed", idx+1) 153 | return 154 | } 155 | } 156 | return 157 | } 158 | -------------------------------------------------------------------------------- /template/tests/subdir/template_4.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asticode/go-astitools/e5e8eaa60d39789dd2189b360d6f83a5ed94b79d/template/tests/subdir/template_4.html -------------------------------------------------------------------------------- /template/tests/template_1.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asticode/go-astitools/e5e8eaa60d39789dd2189b360d6f83a5ed94b79d/template/tests/template_1.html -------------------------------------------------------------------------------- /template/tests/template_2.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asticode/go-astitools/e5e8eaa60d39789dd2189b360d6f83a5ed94b79d/template/tests/template_2.css -------------------------------------------------------------------------------- /template/tests/template_3.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/asticode/go-astitools/e5e8eaa60d39789dd2189b360d6f83a5ed94b79d/template/tests/template_3.html -------------------------------------------------------------------------------- /time/sleep.go: -------------------------------------------------------------------------------- 1 | package astitime 2 | 3 | import ( 4 | "context" 5 | "time" 6 | ) 7 | 8 | // Sleep is a cancellable sleep 9 | func Sleep(ctx context.Context, d time.Duration) (err error) { 10 | for { 11 | select { 12 | case <-time.After(d): 13 | return 14 | case <-ctx.Done(): 15 | err = ctx.Err() 16 | return 17 | } 18 | } 19 | return 20 | } 21 | -------------------------------------------------------------------------------- /time/sleep_test.go: -------------------------------------------------------------------------------- 1 | package astitime_test 2 | 3 | import ( 4 | "sync" 5 | "testing" 6 | "time" 7 | 8 | "github.com/asticode/go-astitools/time" 9 | "github.com/stretchr/testify/assert" 10 | "golang.org/x/net/context" 11 | ) 12 | 13 | func TestSleep(t *testing.T) { 14 | var ctx, cancel = context.WithCancel(context.Background()) 15 | var err error 16 | var wg = &sync.WaitGroup{} 17 | wg.Add(1) 18 | go func() { 19 | defer wg.Done() 20 | err = astitime.Sleep(ctx, time.Minute) 21 | }() 22 | cancel() 23 | wg.Wait() 24 | assert.EqualError(t, err, "context canceled") 25 | } 26 | -------------------------------------------------------------------------------- /time/timestamp.go: -------------------------------------------------------------------------------- 1 | package astitime 2 | 3 | import ( 4 | "bytes" 5 | "strconv" 6 | "time" 7 | ) 8 | 9 | // Timestamp represents a timestamp 10 | type Timestamp struct { 11 | time.Time 12 | } 13 | 14 | // UnmarshalJSON implements the JSONUnmarshaler interface 15 | func (t *Timestamp) UnmarshalJSON(text []byte) error { 16 | return t.UnmarshalText(text) 17 | } 18 | 19 | // UnmarshalText implements the TextUnmarshaler interface 20 | func (t *Timestamp) UnmarshalText(text []byte) (err error) { 21 | var i int 22 | if i, err = strconv.Atoi(string(bytes.Trim(text, "\""))); err != nil { 23 | return 24 | } 25 | t.Time = time.Unix(int64(i), 0) 26 | return 27 | } 28 | 29 | // MarshalJSON implements the JSONMarshaler interface 30 | func (t Timestamp) MarshalJSON() ([]byte, error) { 31 | return t.MarshalText() 32 | } 33 | 34 | // MarshalText implements the TextMarshaler interface 35 | func (t Timestamp) MarshalText() (text []byte, err error) { 36 | text = []byte(strconv.Itoa(int(t.UTC().Unix()))) 37 | return 38 | } 39 | -------------------------------------------------------------------------------- /time/timestamp_test.go: -------------------------------------------------------------------------------- 1 | package astitime_test 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | "time" 7 | 8 | "github.com/asticode/go-astitools/time" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestTimestamp(t *testing.T) { 13 | var tsp = astitime.Timestamp{} 14 | err := json.Unmarshal([]byte("1495290215"), &tsp) 15 | assert.NoError(t, err) 16 | assert.Equal(t, time.Unix(1495290215, 0), tsp.Time) 17 | b, err := json.Marshal(tsp) 18 | assert.NoError(t, err) 19 | assert.Equal(t, "1495290215", string(b)) 20 | } 21 | -------------------------------------------------------------------------------- /unicode/reader.go: -------------------------------------------------------------------------------- 1 | package astiunicode 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "io" 7 | 8 | "github.com/pkg/errors" 9 | "golang.org/x/text/encoding/unicode" 10 | "golang.org/x/text/transform" 11 | ) 12 | 13 | // BOM headers 14 | var ( 15 | bomHeaderUTF32BE = []byte{0x00, 0x00, 0xFE, 0xFF} 16 | bomHeaderUTF32LE = []byte{0xFE, 0xFF, 0x00, 0x00} 17 | bomHeaderUTF16BE = []byte{0xFE, 0xFF} 18 | bomHeaderUTF16LE = []byte{0xFF, 0xFE} 19 | bomHeaderUTF8 = []byte{0xEF, 0xBB, 0xBF} 20 | ) 21 | 22 | // NewReader creates a new unicode reader 23 | func NewReader(i io.Reader) (o io.Reader, err error) { 24 | // Create reader 25 | r := bufio.NewReader(i) 26 | 27 | // Read first 4 bytes 28 | var b []byte 29 | if b, err = r.Peek(4); err != nil { 30 | err = errors.Wrap(err, "astiunicode: reading first 4 bytes failed") 31 | return 32 | } 33 | 34 | // Create transformer 35 | if bytes.HasPrefix(b, bomHeaderUTF32BE) || bytes.HasPrefix(b, bomHeaderUTF32LE) { 36 | err = errors.New("astiunicode: UTF32 is not handled yet") 37 | return 38 | } else if bytes.HasPrefix(b, bomHeaderUTF16BE) { 39 | o = transform.NewReader(r, unicode.UTF16(unicode.BigEndian, unicode.IgnoreBOM).NewDecoder()) 40 | } else if bytes.HasPrefix(b, bomHeaderUTF16LE) { 41 | o = transform.NewReader(r, unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewDecoder()) 42 | } else if bytes.HasPrefix(b, bomHeaderUTF8) { 43 | o = transform.NewReader(r, unicode.UTF8.NewDecoder()) 44 | } else { 45 | o = r 46 | } 47 | return 48 | } 49 | -------------------------------------------------------------------------------- /unicode/utf8.go: -------------------------------------------------------------------------------- 1 | package astiunicode 2 | 3 | import ( 4 | "unicode/utf8" 5 | ) 6 | 7 | // StripUTF8Chars strips UTF8 chars 8 | func StripUTF8Chars(i []byte) (o []byte) { 9 | buf := i 10 | for len(buf) > 0 { 11 | r, size := utf8.DecodeRune(buf) 12 | if r == utf8.RuneError && size == 1 { 13 | buf = buf[size:] 14 | continue 15 | } 16 | o = append(o, buf[:size]...) 17 | buf = buf[size:] 18 | } 19 | return 20 | } 21 | -------------------------------------------------------------------------------- /unicode/utf8_test.go: -------------------------------------------------------------------------------- 1 | package astiunicode 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestStripUTF8Chars(t *testing.T) { 10 | assert.Equal(t, []byte("az"), StripUTF8Chars([]byte("az"))) 11 | assert.Equal(t, []byte("az"), StripUTF8Chars([]byte("a\xc5z"))) 12 | } 13 | -------------------------------------------------------------------------------- /url/url.go: -------------------------------------------------------------------------------- 1 | package astiurl 2 | 3 | import ( 4 | "net/url" 5 | "path/filepath" 6 | 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | // Parse parses an URL (files included) 11 | func Parse(i string) (o *url.URL, err error) { 12 | // Basic parse 13 | if o, err = url.Parse(i); err != nil { 14 | err = errors.Wrapf(err, "basic parsing of url %s failed", i) 15 | return 16 | } 17 | 18 | // File 19 | if o.Scheme == "" { 20 | // Get absolute path 21 | if i, err = filepath.Abs(i); err != nil { 22 | err = errors.Wrapf(err, "getting absolute path of %s failed", i) 23 | return 24 | } 25 | 26 | // Set url 27 | o = &url.URL{Path: filepath.ToSlash(i), Scheme: "file"} 28 | } 29 | return 30 | } 31 | -------------------------------------------------------------------------------- /worker/README.md: -------------------------------------------------------------------------------- 1 | # Worker 2 | 3 | ```go 4 | // Init worker 5 | var w = astiworker.NewWorker() 6 | 7 | // Handle signals 8 | w.HandleSignals() 9 | 10 | // Serve 11 | w.Serve("127.0.0.1:4000", myHandler) 12 | 13 | // Execute 14 | h, _ := w.Exec("sleep", "10") 15 | go func() { 16 | time.Sleep(3 * time.Second) 17 | h.Stop() 18 | } 19 | 20 | // Wait 21 | w.Wait() 22 | ``` -------------------------------------------------------------------------------- /worker/amqp.go: -------------------------------------------------------------------------------- 1 | package astiworker 2 | 3 | import ( 4 | "math" 5 | 6 | "github.com/asticode/go-astiamqp" 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | // ConfigurationConsumer represents a consumer configuration 11 | type ConfigurationConsumer struct { 12 | AMQP astiamqp.ConfigurationConsumer 13 | WorkerCount int 14 | } 15 | 16 | // Consume consumes AMQP events 17 | func (w *Worker) Consume(a *astiamqp.AMQP, cs ...ConfigurationConsumer) (err error) { 18 | // Loop through configurations 19 | for idxConf, c := range cs { 20 | // Loop through workers 21 | for idxWorker := 0; idxWorker < int(math.Max(1, float64(c.WorkerCount))); idxWorker++ { 22 | if err = a.AddConsumer(c.AMQP); err != nil { 23 | err = errors.Wrapf(err, "main: adding consumer #%d for conf #%d %+v failed", idxWorker+1, idxConf+1, c) 24 | return 25 | } 26 | } 27 | } 28 | 29 | // Execute in a task 30 | w.NewTask().Do(func() { 31 | // Wait for context to be done 32 | <-w.Context().Done() 33 | 34 | // Stop amqp 35 | a.Stop() 36 | }) 37 | return 38 | } 39 | -------------------------------------------------------------------------------- /worker/dial.go: -------------------------------------------------------------------------------- 1 | package astiworker 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | astilog "github.com/asticode/go-astilog" 8 | astiws "github.com/asticode/go-astiws" 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | // DialOptions represents dial options 13 | type DialOptions struct { 14 | Addr string 15 | Client *astiws.Client 16 | Header http.Header 17 | OnDial func() error 18 | OnReadError func(err error) 19 | } 20 | 21 | // Dial dials with options 22 | // It's the responsibility of the caller to close the Client 23 | func (w *Worker) Dial(o DialOptions) { 24 | // Execute in a task 25 | w.NewTask().Do(func() { 26 | // Dial 27 | go func() { 28 | const sleepError = 5 * time.Second 29 | for { 30 | // Check context error 31 | if w.ctx.Err() != nil { 32 | break 33 | } 34 | 35 | // Dial 36 | astilog.Infof("astiworker: dialing %s", o.Addr) 37 | if err := o.Client.DialWithHeaders(o.Addr, o.Header); err != nil { 38 | astilog.Error(errors.Wrapf(err, "astiworker: dialing %s failed", o.Addr)) 39 | time.Sleep(sleepError) 40 | continue 41 | } 42 | 43 | // Custom callback 44 | if o.OnDial != nil { 45 | if err := o.OnDial(); err != nil { 46 | astilog.Error(errors.Wrapf(err, "astiworker: custom on dial callback on %s failed", o.Addr)) 47 | time.Sleep(sleepError) 48 | continue 49 | } 50 | } 51 | 52 | // Read 53 | if err := o.Client.Read(); err != nil { 54 | if o.OnReadError != nil { 55 | o.OnReadError(err) 56 | } else { 57 | astilog.Error(errors.Wrapf(err, "astiworker: reading on %s failed", o.Addr)) 58 | } 59 | time.Sleep(sleepError) 60 | continue 61 | } 62 | } 63 | }() 64 | 65 | // Wait for context to be done 66 | <-w.ctx.Done() 67 | }) 68 | 69 | } 70 | -------------------------------------------------------------------------------- /worker/executer.go: -------------------------------------------------------------------------------- 1 | package astiworker 2 | 3 | import ( 4 | "context" 5 | "os/exec" 6 | "strings" 7 | "sync" 8 | 9 | "github.com/asticode/go-astilog" 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | // Statuses 14 | const ( 15 | StatusCrashed = "crashed" 16 | StatusRunning = "running" 17 | StatusStopped = "stopped" 18 | ) 19 | 20 | // ExecHandler represents an object capable of handling the execution of a cmd 21 | type ExecHandler interface { 22 | Status() string 23 | Stop() 24 | } 25 | 26 | type defaultExecHandler struct { 27 | cancel context.CancelFunc 28 | ctx context.Context 29 | err error 30 | o sync.Once 31 | stopped bool 32 | } 33 | 34 | func (h *defaultExecHandler) Status() string { 35 | if h.ctx.Err() != nil { 36 | if h.stopped || h.err == nil { 37 | return StatusStopped 38 | } 39 | return StatusCrashed 40 | } 41 | return StatusRunning 42 | } 43 | 44 | func (h *defaultExecHandler) Stop() { 45 | h.o.Do(func() { 46 | h.cancel() 47 | h.stopped = true 48 | }) 49 | } 50 | 51 | type ExecOptions struct { 52 | Args []string 53 | CmdAdapter func(cmd *exec.Cmd, h ExecHandler) error 54 | Name string 55 | StopFunc func(cmd *exec.Cmd) error 56 | } 57 | 58 | // Exec executes a cmd 59 | // The process will be stopped when the worker stops 60 | func (w *Worker) Exec(o ExecOptions) (ExecHandler, error) { 61 | // Create handler 62 | h := &defaultExecHandler{} 63 | h.ctx, h.cancel = context.WithCancel(w.Context()) 64 | 65 | // Create command 66 | cmd := exec.Command(o.Name, o.Args...) 67 | 68 | // Adapt command 69 | if o.CmdAdapter != nil { 70 | if err := o.CmdAdapter(cmd, h); err != nil { 71 | return nil, errors.Wrap(err, "astiworker: adapting cmd failed") 72 | } 73 | } 74 | 75 | // Start 76 | astilog.Infof("astiworker: starting %s", strings.Join(cmd.Args, " ")) 77 | if err := cmd.Start(); err != nil { 78 | err = errors.Wrapf(err, "astiworker: executing %s", strings.Join(cmd.Args, " ")) 79 | return nil, err 80 | } 81 | 82 | // Handle context 83 | go func() { 84 | // Wait for context to be done 85 | <-h.ctx.Done() 86 | 87 | // Get stop func 88 | f := func() error { return cmd.Process.Kill() } 89 | if o.StopFunc != nil { 90 | f = func() error { return o.StopFunc(cmd) } 91 | } 92 | 93 | // Stop 94 | if err := f(); err != nil { 95 | astilog.Error(errors.Wrap(err, "astiworker: stopping cmd failed")) 96 | return 97 | } 98 | }() 99 | 100 | // Execute in a task 101 | w.NewTask().Do(func() { 102 | h.err = cmd.Wait() 103 | h.cancel() 104 | astilog.Infof("astiworker: status is now %s for %s", h.Status(), strings.Join(cmd.Args, " ")) 105 | }) 106 | return h, nil 107 | } 108 | -------------------------------------------------------------------------------- /worker/server.go: -------------------------------------------------------------------------------- 1 | package astiworker 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | 7 | "github.com/asticode/go-astilog" 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | // Serve spawns a server 12 | func (w *Worker) Serve(addr string, h http.Handler) { 13 | // Create server 14 | s := &http.Server{Addr: addr, Handler: h} 15 | 16 | // Execute in a task 17 | w.NewTask().Do(func() { 18 | // Log 19 | astilog.Infof("astiworker: serving on %s", addr) 20 | 21 | // Serve 22 | var chanDone = make(chan error) 23 | go func() { 24 | if err := s.ListenAndServe(); err != nil { 25 | chanDone <- err 26 | } 27 | }() 28 | 29 | // Wait for context or chanDone to be done 30 | select { 31 | case <-w.ctx.Done(): 32 | if w.ctx.Err() != context.Canceled { 33 | astilog.Error(errors.Wrap(w.ctx.Err(), "astiworker: context error")) 34 | } 35 | case err := <-chanDone: 36 | if err != nil { 37 | astilog.Error(errors.Wrap(err, "astiworker: serving failed")) 38 | } 39 | } 40 | 41 | // Shutdown 42 | astilog.Infof("astiworker: shutting down server on %s", addr) 43 | if err := s.Shutdown(context.Background()); err != nil { 44 | astilog.Error(errors.Wrapf(err, "astiworker: shutting down server on %s failed", addr)) 45 | } 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /worker/task.go: -------------------------------------------------------------------------------- 1 | package astiworker 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | // Task represents a task 8 | type Task struct { 9 | od, ow sync.Once 10 | wg, pwg *sync.WaitGroup 11 | } 12 | 13 | func newTask(parentWg *sync.WaitGroup) (t *Task) { 14 | t = &Task{ 15 | wg: &sync.WaitGroup{}, 16 | pwg: parentWg, 17 | } 18 | t.pwg.Add(1) 19 | return 20 | } 21 | 22 | // TaskFunc represents a function that can create a new task 23 | type TaskFunc func() *Task 24 | 25 | // NewSubTask creates a new sub task 26 | func (t *Task) NewSubTask() *Task { 27 | return newTask(t.wg) 28 | } 29 | 30 | // Do executes the task 31 | func (t *Task) Do(f func()) { 32 | go func() { 33 | // Make sure to mark the task as done 34 | defer t.Done() 35 | 36 | // Custom 37 | f() 38 | }() 39 | } 40 | 41 | // Done indicates the task is done 42 | func (t *Task) Done() { 43 | t.od.Do(func() { 44 | t.pwg.Done() 45 | }) 46 | } 47 | 48 | // Wait waits for the task to be finished 49 | func (t *Task) Wait() { 50 | t.ow.Do(func() { 51 | t.wg.Wait() 52 | }) 53 | } 54 | -------------------------------------------------------------------------------- /worker/worker.go: -------------------------------------------------------------------------------- 1 | package astiworker 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "os/signal" 7 | "sync" 8 | "syscall" 9 | 10 | "github.com/asticode/go-astilog" 11 | ) 12 | 13 | // Worker represents an object capable of blocking, handling signals and stopping 14 | type Worker struct { 15 | cancel context.CancelFunc 16 | ctx context.Context 17 | os, ow sync.Once 18 | wg *sync.WaitGroup 19 | } 20 | 21 | // NewWorker builds a new worker 22 | func NewWorker() (w *Worker) { 23 | astilog.Info("astiworker: starting worker...") 24 | w = &Worker{wg: &sync.WaitGroup{}} 25 | w.ctx, w.cancel = context.WithCancel(context.Background()) 26 | w.wg.Add(1) 27 | return 28 | } 29 | 30 | // SignalHandler represents a func that can handle a signal 31 | type SignalHandler func(s os.Signal) 32 | 33 | func isTermSignal(s os.Signal) bool { 34 | return s == syscall.SIGABRT || s == syscall.SIGKILL || s == syscall.SIGINT || s == syscall.SIGQUIT || s == syscall.SIGTERM 35 | } 36 | 37 | // TermSignalHandler returns a SignalHandler that is executed only on a term signal 38 | func TermSignalHandler(f func()) SignalHandler { 39 | return func(s os.Signal) { 40 | if isTermSignal(s) { 41 | f() 42 | } 43 | } 44 | } 45 | 46 | // HandleSignals handles signals 47 | func (w *Worker) HandleSignals(hs ...SignalHandler) { 48 | // Add default handler 49 | hs = append([]SignalHandler{TermSignalHandler(w.Stop)}, hs...) 50 | 51 | // Notify 52 | ch := make(chan os.Signal, 1) 53 | signal.Notify(ch) 54 | 55 | // Execute in a task 56 | w.NewTask().Do(func() { 57 | for { 58 | select { 59 | case s := <-ch: 60 | // Log 61 | astilog.Debugf("astiworker: received signal %s", s) 62 | 63 | // Loop through handlers 64 | for _, h := range hs { 65 | h(s) 66 | } 67 | 68 | // Return 69 | if isTermSignal(s) { 70 | return 71 | } 72 | case <-w.Context().Done(): 73 | return 74 | } 75 | } 76 | }) 77 | } 78 | 79 | // Stop stops the Worker 80 | func (w *Worker) Stop() { 81 | w.os.Do(func() { 82 | astilog.Info("astiworker: stopping worker...") 83 | w.cancel() 84 | w.wg.Done() 85 | }) 86 | } 87 | 88 | // Wait is a blocking pattern 89 | func (w *Worker) Wait() { 90 | w.ow.Do(func() { 91 | astilog.Info("astiworker: worker is now waiting...") 92 | w.wg.Wait() 93 | }) 94 | } 95 | 96 | // NewTask creates a new task 97 | func (w *Worker) NewTask() *Task { 98 | return newTask(w.wg) 99 | } 100 | 101 | // Context returns the worker's context 102 | func (w *Worker) Context() context.Context { 103 | return w.ctx 104 | } 105 | --------------------------------------------------------------------------------