github.com/ferranbt/nomad@v0.9.3-0.20190607002617-85c449b7667c/client/fs_endpoint.go (about)

     1  package client
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"io"
     8  	"math"
     9  	"net/http"
    10  	"os"
    11  	"path/filepath"
    12  	"sort"
    13  	"strconv"
    14  	"strings"
    15  	"syscall"
    16  	"time"
    17  
    18  	metrics "github.com/armon/go-metrics"
    19  	"github.com/hashicorp/nomad/acl"
    20  	"github.com/hashicorp/nomad/client/allocdir"
    21  	sframer "github.com/hashicorp/nomad/client/lib/streamframer"
    22  	cstructs "github.com/hashicorp/nomad/client/structs"
    23  	"github.com/hashicorp/nomad/helper"
    24  	"github.com/hashicorp/nomad/nomad/structs"
    25  	"github.com/hpcloud/tail/watch"
    26  	"github.com/ugorji/go/codec"
    27  )
    28  
    29  var (
    30  	allocIDNotPresentErr = fmt.Errorf("must provide a valid alloc id")
    31  	pathNotPresentErr    = fmt.Errorf("must provide a file path")
    32  	taskNotPresentErr    = fmt.Errorf("must provide task name")
    33  	logTypeNotPresentErr = fmt.Errorf("must provide log type (stdout/stderr)")
    34  	invalidOrigin        = fmt.Errorf("origin must be start or end")
    35  )
    36  
    37  const (
    38  	// streamFramesBuffer is the number of stream frames that will be buffered
    39  	// before back pressure is applied on the stream framer.
    40  	streamFramesBuffer = 32
    41  
    42  	// streamFrameSize is the maximum number of bytes to send in a single frame
    43  	streamFrameSize = 64 * 1024
    44  
    45  	// streamHeartbeatRate is the rate at which a heartbeat will occur to detect
    46  	// a closed connection without sending any additional data
    47  	streamHeartbeatRate = 1 * time.Second
    48  
    49  	// streamBatchWindow is the window in which file content is batched before
    50  	// being flushed if the frame size has not been hit.
    51  	streamBatchWindow = 200 * time.Millisecond
    52  
    53  	// nextLogCheckRate is the rate at which we check for a log entry greater
    54  	// than what we are watching for. This is to handle the case in which logs
    55  	// rotate faster than we can detect and we have to rely on a normal
    56  	// directory listing.
    57  	nextLogCheckRate = 100 * time.Millisecond
    58  
    59  	// deleteEvent and truncateEvent are the file events that can be sent in a
    60  	// StreamFrame
    61  	deleteEvent   = "file deleted"
    62  	truncateEvent = "file truncated"
    63  
    64  	// OriginStart and OriginEnd are the available parameters for the origin
    65  	// argument when streaming a file. They respectively offset from the start
    66  	// and end of a file.
    67  	OriginStart = "start"
    68  	OriginEnd   = "end"
    69  )
    70  
    71  // FileSystem endpoint is used for accessing the logs and filesystem of
    72  // allocations.
    73  type FileSystem struct {
    74  	c *Client
    75  }
    76  
    77  func NewFileSystemEndpoint(c *Client) *FileSystem {
    78  	f := &FileSystem{c}
    79  	f.c.streamingRpcs.Register("FileSystem.Logs", f.logs)
    80  	f.c.streamingRpcs.Register("FileSystem.Stream", f.stream)
    81  	return f
    82  }
    83  
    84  // handleStreamResultError is a helper for sending an error with a potential
    85  // error code. The transmission of the error is ignored if the error has been
    86  // generated by the closing of the underlying transport.
    87  func handleStreamResultError(err error, code *int64, encoder *codec.Encoder) {
    88  	// Nothing to do as the conn is closed
    89  	if err == io.EOF || strings.Contains(err.Error(), "closed") {
    90  		return
    91  	}
    92  
    93  	encoder.Encode(&cstructs.StreamErrWrapper{
    94  		Error: cstructs.NewRpcError(err, code),
    95  	})
    96  }
    97  
    98  // List is used to list the contents of an allocation's directory.
    99  func (f *FileSystem) List(args *cstructs.FsListRequest, reply *cstructs.FsListResponse) error {
   100  	defer metrics.MeasureSince([]string{"client", "file_system", "list"}, time.Now())
   101  
   102  	// Check read permissions
   103  	if aclObj, err := f.c.ResolveToken(args.QueryOptions.AuthToken); err != nil {
   104  		return err
   105  	} else if aclObj != nil && !aclObj.AllowNsOp(args.Namespace, acl.NamespaceCapabilityReadFS) {
   106  		return structs.ErrPermissionDenied
   107  	}
   108  
   109  	fs, err := f.c.GetAllocFS(args.AllocID)
   110  	if err != nil {
   111  		return err
   112  	}
   113  	files, err := fs.List(args.Path)
   114  	if err != nil {
   115  		return err
   116  	}
   117  
   118  	reply.Files = files
   119  	return nil
   120  }
   121  
   122  // Stat is used to stat a file in the allocation's directory.
   123  func (f *FileSystem) Stat(args *cstructs.FsStatRequest, reply *cstructs.FsStatResponse) error {
   124  	defer metrics.MeasureSince([]string{"client", "file_system", "stat"}, time.Now())
   125  
   126  	// Check read permissions
   127  	if aclObj, err := f.c.ResolveToken(args.QueryOptions.AuthToken); err != nil {
   128  		return err
   129  	} else if aclObj != nil && !aclObj.AllowNsOp(args.Namespace, acl.NamespaceCapabilityReadFS) {
   130  		return structs.ErrPermissionDenied
   131  	}
   132  
   133  	fs, err := f.c.GetAllocFS(args.AllocID)
   134  	if err != nil {
   135  		return err
   136  	}
   137  	info, err := fs.Stat(args.Path)
   138  	if err != nil {
   139  		return err
   140  	}
   141  
   142  	reply.Info = info
   143  	return nil
   144  }
   145  
   146  // stream is is used to stream the contents of file in an allocation's
   147  // directory.
   148  func (f *FileSystem) stream(conn io.ReadWriteCloser) {
   149  	defer metrics.MeasureSince([]string{"client", "file_system", "stream"}, time.Now())
   150  	defer conn.Close()
   151  
   152  	// Decode the arguments
   153  	var req cstructs.FsStreamRequest
   154  	decoder := codec.NewDecoder(conn, structs.MsgpackHandle)
   155  	encoder := codec.NewEncoder(conn, structs.MsgpackHandle)
   156  
   157  	if err := decoder.Decode(&req); err != nil {
   158  		handleStreamResultError(err, helper.Int64ToPtr(500), encoder)
   159  		return
   160  	}
   161  
   162  	// Check read permissions
   163  	if aclObj, err := f.c.ResolveToken(req.QueryOptions.AuthToken); err != nil {
   164  		handleStreamResultError(err, helper.Int64ToPtr(403), encoder)
   165  		return
   166  	} else if aclObj != nil && !aclObj.AllowNsOp(req.Namespace, acl.NamespaceCapabilityReadFS) {
   167  		handleStreamResultError(structs.ErrPermissionDenied, helper.Int64ToPtr(403), encoder)
   168  		return
   169  	}
   170  
   171  	// Validate the arguments
   172  	if req.AllocID == "" {
   173  		handleStreamResultError(allocIDNotPresentErr, helper.Int64ToPtr(400), encoder)
   174  		return
   175  	}
   176  	if req.Path == "" {
   177  		handleStreamResultError(pathNotPresentErr, helper.Int64ToPtr(400), encoder)
   178  		return
   179  	}
   180  	switch req.Origin {
   181  	case "start", "end":
   182  	case "":
   183  		req.Origin = "start"
   184  	default:
   185  		handleStreamResultError(invalidOrigin, helper.Int64ToPtr(400), encoder)
   186  		return
   187  	}
   188  
   189  	fs, err := f.c.GetAllocFS(req.AllocID)
   190  	if err != nil {
   191  		code := helper.Int64ToPtr(500)
   192  		if structs.IsErrUnknownAllocation(err) {
   193  			code = helper.Int64ToPtr(404)
   194  		}
   195  
   196  		handleStreamResultError(err, code, encoder)
   197  		return
   198  	}
   199  
   200  	// Calculate the offset
   201  	fileInfo, err := fs.Stat(req.Path)
   202  	if err != nil {
   203  		handleStreamResultError(err, helper.Int64ToPtr(400), encoder)
   204  		return
   205  	}
   206  	if fileInfo.IsDir {
   207  		handleStreamResultError(
   208  			fmt.Errorf("file %q is a directory", req.Path),
   209  			helper.Int64ToPtr(400), encoder)
   210  		return
   211  	}
   212  
   213  	// If offsetting from the end subtract from the size
   214  	if req.Origin == "end" {
   215  		req.Offset = fileInfo.Size - req.Offset
   216  		if req.Offset < 0 {
   217  			req.Offset = 0
   218  		}
   219  	}
   220  
   221  	frames := make(chan *sframer.StreamFrame, streamFramesBuffer)
   222  	errCh := make(chan error)
   223  	var buf bytes.Buffer
   224  	frameCodec := codec.NewEncoder(&buf, structs.JsonHandle)
   225  
   226  	// Create the framer
   227  	framer := sframer.NewStreamFramer(frames, streamHeartbeatRate, streamBatchWindow, streamFrameSize)
   228  	framer.Run()
   229  	defer framer.Destroy()
   230  
   231  	// If we aren't following end as soon as we hit EOF
   232  	var eofCancelCh chan error
   233  	if !req.Follow {
   234  		eofCancelCh = make(chan error)
   235  		close(eofCancelCh)
   236  	}
   237  
   238  	ctx, cancel := context.WithCancel(context.Background())
   239  	defer cancel()
   240  
   241  	// Start streaming
   242  	go func() {
   243  		if err := f.streamFile(ctx, req.Offset, req.Path, req.Limit, fs, framer, eofCancelCh); err != nil {
   244  			select {
   245  			case errCh <- err:
   246  			case <-ctx.Done():
   247  			}
   248  		}
   249  
   250  		framer.Destroy()
   251  	}()
   252  
   253  	// Create a goroutine to detect the remote side closing
   254  	go func() {
   255  		for {
   256  			if _, err := conn.Read(nil); err != nil {
   257  				if err == io.EOF || err == io.ErrClosedPipe {
   258  					// One end of the pipe was explicitly closed, exit cleanly
   259  					cancel()
   260  					return
   261  				}
   262  				select {
   263  				case errCh <- err:
   264  				case <-ctx.Done():
   265  					return
   266  				}
   267  			}
   268  		}
   269  	}()
   270  
   271  	var streamErr error
   272  OUTER:
   273  	for {
   274  		select {
   275  		case streamErr = <-errCh:
   276  			break OUTER
   277  		case frame, ok := <-frames:
   278  			if !ok {
   279  				// frame may have been closed when an error
   280  				// occurred. Check once more for an error.
   281  				select {
   282  				case streamErr = <-errCh:
   283  					// There was a pending error!
   284  				default:
   285  					// No error, continue on
   286  				}
   287  
   288  				break OUTER
   289  			}
   290  
   291  			var resp cstructs.StreamErrWrapper
   292  			if req.PlainText {
   293  				resp.Payload = frame.Data
   294  			} else {
   295  				if err = frameCodec.Encode(frame); err != nil {
   296  					streamErr = err
   297  					break OUTER
   298  				}
   299  
   300  				resp.Payload = buf.Bytes()
   301  				buf.Reset()
   302  			}
   303  
   304  			if err := encoder.Encode(resp); err != nil {
   305  				streamErr = err
   306  				break OUTER
   307  			}
   308  			encoder.Reset(conn)
   309  		case <-ctx.Done():
   310  			break OUTER
   311  		}
   312  	}
   313  
   314  	if streamErr != nil {
   315  		handleStreamResultError(streamErr, helper.Int64ToPtr(500), encoder)
   316  		return
   317  	}
   318  }
   319  
   320  // logs is is used to stream a task's logs.
   321  func (f *FileSystem) logs(conn io.ReadWriteCloser) {
   322  	defer metrics.MeasureSince([]string{"client", "file_system", "logs"}, time.Now())
   323  	defer conn.Close()
   324  
   325  	// Decode the arguments
   326  	var req cstructs.FsLogsRequest
   327  	decoder := codec.NewDecoder(conn, structs.MsgpackHandle)
   328  	encoder := codec.NewEncoder(conn, structs.MsgpackHandle)
   329  
   330  	if err := decoder.Decode(&req); err != nil {
   331  		handleStreamResultError(err, helper.Int64ToPtr(500), encoder)
   332  		return
   333  	}
   334  
   335  	// Check read permissions
   336  	if aclObj, err := f.c.ResolveToken(req.QueryOptions.AuthToken); err != nil {
   337  		handleStreamResultError(err, nil, encoder)
   338  		return
   339  	} else if aclObj != nil {
   340  		readfs := aclObj.AllowNsOp(req.QueryOptions.Namespace, acl.NamespaceCapabilityReadFS)
   341  		logs := aclObj.AllowNsOp(req.QueryOptions.Namespace, acl.NamespaceCapabilityReadLogs)
   342  		if !readfs && !logs {
   343  			handleStreamResultError(structs.ErrPermissionDenied, nil, encoder)
   344  			return
   345  		}
   346  	}
   347  
   348  	// Validate the arguments
   349  	if req.AllocID == "" {
   350  		handleStreamResultError(allocIDNotPresentErr, helper.Int64ToPtr(400), encoder)
   351  		return
   352  	}
   353  	if req.Task == "" {
   354  		handleStreamResultError(taskNotPresentErr, helper.Int64ToPtr(400), encoder)
   355  		return
   356  	}
   357  	switch req.LogType {
   358  	case "stdout", "stderr":
   359  	default:
   360  		handleStreamResultError(logTypeNotPresentErr, helper.Int64ToPtr(400), encoder)
   361  		return
   362  	}
   363  	switch req.Origin {
   364  	case "start", "end":
   365  	case "":
   366  		req.Origin = "start"
   367  	default:
   368  		handleStreamResultError(invalidOrigin, helper.Int64ToPtr(400), encoder)
   369  		return
   370  	}
   371  
   372  	fs, err := f.c.GetAllocFS(req.AllocID)
   373  	if err != nil {
   374  		code := helper.Int64ToPtr(500)
   375  		if structs.IsErrUnknownAllocation(err) {
   376  			code = helper.Int64ToPtr(404)
   377  		}
   378  
   379  		handleStreamResultError(err, code, encoder)
   380  		return
   381  	}
   382  
   383  	allocState, err := f.c.GetAllocState(req.AllocID)
   384  	if err != nil {
   385  		code := helper.Int64ToPtr(500)
   386  		if structs.IsErrUnknownAllocation(err) {
   387  			code = helper.Int64ToPtr(404)
   388  		}
   389  
   390  		handleStreamResultError(err, code, encoder)
   391  		return
   392  	}
   393  
   394  	// Check that the task is there
   395  	taskState := allocState.TaskStates[req.Task]
   396  	if taskState == nil {
   397  		handleStreamResultError(
   398  			fmt.Errorf("unknown task name %q", req.Task),
   399  			helper.Int64ToPtr(400),
   400  			encoder)
   401  		return
   402  	}
   403  
   404  	if taskState.StartedAt.IsZero() {
   405  		handleStreamResultError(
   406  			fmt.Errorf("task %q not started yet. No logs available", req.Task),
   407  			helper.Int64ToPtr(404),
   408  			encoder)
   409  		return
   410  	}
   411  
   412  	ctx, cancel := context.WithCancel(context.Background())
   413  	defer cancel()
   414  
   415  	frames := make(chan *sframer.StreamFrame, streamFramesBuffer)
   416  	errCh := make(chan error)
   417  
   418  	// Start streaming
   419  	go func() {
   420  		if err := f.logsImpl(ctx, req.Follow, req.PlainText,
   421  			req.Offset, req.Origin, req.Task, req.LogType, fs, frames); err != nil {
   422  			select {
   423  			case errCh <- err:
   424  			case <-ctx.Done():
   425  			}
   426  		}
   427  	}()
   428  
   429  	// Create a goroutine to detect the remote side closing
   430  	go func() {
   431  		for {
   432  			if _, err := conn.Read(nil); err != nil {
   433  				if err == io.EOF || err == io.ErrClosedPipe {
   434  					// One end of the pipe was explicitly closed, exit cleanly
   435  					cancel()
   436  					return
   437  				}
   438  				select {
   439  				case errCh <- err:
   440  				case <-ctx.Done():
   441  				}
   442  				return
   443  			}
   444  		}
   445  	}()
   446  
   447  	var streamErr error
   448  	buf := new(bytes.Buffer)
   449  	frameCodec := codec.NewEncoder(buf, structs.JsonHandle)
   450  OUTER:
   451  	for {
   452  		select {
   453  		case streamErr = <-errCh:
   454  			break OUTER
   455  		case frame, ok := <-frames:
   456  			if !ok {
   457  				// framer may have been closed when an error
   458  				// occurred. Check once more for an error.
   459  				select {
   460  				case streamErr = <-errCh:
   461  					// There was a pending error!
   462  				default:
   463  					// No error, continue on
   464  				}
   465  
   466  				break OUTER
   467  			}
   468  
   469  			var resp cstructs.StreamErrWrapper
   470  			if req.PlainText {
   471  				resp.Payload = frame.Data
   472  			} else {
   473  				if err = frameCodec.Encode(frame); err != nil {
   474  					streamErr = err
   475  					break OUTER
   476  				}
   477  				frameCodec.Reset(buf)
   478  
   479  				resp.Payload = buf.Bytes()
   480  				buf.Reset()
   481  			}
   482  
   483  			if err := encoder.Encode(resp); err != nil {
   484  				streamErr = err
   485  				break OUTER
   486  			}
   487  			encoder.Reset(conn)
   488  		}
   489  	}
   490  
   491  	if streamErr != nil {
   492  		// If error has a Code, use it
   493  		var code int64 = 500
   494  		if codedErr, ok := streamErr.(interface{ Code() int }); ok {
   495  			code = int64(codedErr.Code())
   496  		}
   497  		handleStreamResultError(streamErr, &code, encoder)
   498  		return
   499  	}
   500  }
   501  
   502  // logsImpl is used to stream the logs of a the given task. Output is sent on
   503  // the passed frames channel and the method will return on EOF if follow is not
   504  // true otherwise when the context is cancelled or on an error.
   505  func (f *FileSystem) logsImpl(ctx context.Context, follow, plain bool, offset int64,
   506  	origin, task, logType string,
   507  	fs allocdir.AllocDirFS, frames chan<- *sframer.StreamFrame) error {
   508  
   509  	// Create the framer
   510  	framer := sframer.NewStreamFramer(frames, streamHeartbeatRate, streamBatchWindow, streamFrameSize)
   511  	framer.Run()
   512  	defer framer.Destroy()
   513  
   514  	// Path to the logs
   515  	logPath := filepath.Join(allocdir.SharedAllocName, allocdir.LogDirName)
   516  
   517  	// nextIdx is the next index to read logs from
   518  	var nextIdx int64
   519  	switch origin {
   520  	case "start":
   521  		nextIdx = 0
   522  	case "end":
   523  		nextIdx = math.MaxInt64
   524  		offset *= -1
   525  	default:
   526  		return invalidOrigin
   527  	}
   528  
   529  	for {
   530  		// Logic for picking next file is:
   531  		// 1) List log files
   532  		// 2) Pick log file closest to desired index
   533  		// 3) Open log file at correct offset
   534  		// 3a) No error, read contents
   535  		// 3b) If file doesn't exist, goto 1 as it may have been rotated out
   536  		entries, err := fs.List(logPath)
   537  		if err != nil {
   538  			return fmt.Errorf("failed to list entries: %v", err)
   539  		}
   540  
   541  		// If we are not following logs, determine the max index for the logs we are
   542  		// interested in so we can stop there.
   543  		maxIndex := int64(math.MaxInt64)
   544  		if !follow {
   545  			_, idx, _, err := findClosest(entries, maxIndex, 0, task, logType)
   546  			if err != nil {
   547  				return err
   548  			}
   549  			maxIndex = idx
   550  		}
   551  
   552  		logEntry, idx, openOffset, err := findClosest(entries, nextIdx, offset, task, logType)
   553  		if err != nil {
   554  			return err
   555  		}
   556  
   557  		var eofCancelCh chan error
   558  		exitAfter := false
   559  		if !follow && idx > maxIndex {
   560  			// Exceeded what was there initially so return
   561  			return nil
   562  		} else if !follow && idx == maxIndex {
   563  			// At the end
   564  			eofCancelCh = make(chan error)
   565  			close(eofCancelCh)
   566  			exitAfter = true
   567  		} else {
   568  			eofCancelCh = blockUntilNextLog(ctx, fs, logPath, task, logType, idx+1)
   569  		}
   570  
   571  		p := filepath.Join(logPath, logEntry.Name)
   572  		err = f.streamFile(ctx, openOffset, p, 0, fs, framer, eofCancelCh)
   573  
   574  		// Check if the context is cancelled
   575  		select {
   576  		case <-ctx.Done():
   577  			return nil
   578  		default:
   579  		}
   580  
   581  		if err != nil {
   582  			// Check if there was an error where the file does not exist. That means
   583  			// it got rotated out from under us.
   584  			if os.IsNotExist(err) {
   585  				continue
   586  			}
   587  
   588  			// Check if the connection was closed
   589  			if err == syscall.EPIPE {
   590  				return nil
   591  			}
   592  
   593  			return fmt.Errorf("failed to stream %q: %v", p, err)
   594  		}
   595  
   596  		if exitAfter {
   597  			return nil
   598  		}
   599  
   600  		// defensively check to make sure StreamFramer hasn't stopped
   601  		// running to avoid tight loops with goroutine leaks as in
   602  		// #3342
   603  		select {
   604  		case <-framer.ExitCh():
   605  			return nil
   606  		default:
   607  		}
   608  
   609  		// Since we successfully streamed, update the overall offset/idx.
   610  		offset = int64(0)
   611  		nextIdx = idx + 1
   612  	}
   613  }
   614  
   615  // streamFile is the internal method to stream the content of a file. If limit
   616  // is greater than zero, the stream will end once that many bytes have been
   617  // read. eofCancelCh is used to cancel the stream if triggered while at EOF. If
   618  // the connection is broken an EPIPE error is returned
   619  func (f *FileSystem) streamFile(ctx context.Context, offset int64, path string, limit int64,
   620  	fs allocdir.AllocDirFS, framer *sframer.StreamFramer, eofCancelCh chan error) error {
   621  
   622  	// Get the reader
   623  	file, err := fs.ReadAt(path, offset)
   624  	if err != nil {
   625  		return err
   626  	}
   627  	defer file.Close()
   628  
   629  	var fileReader io.Reader
   630  	if limit <= 0 {
   631  		fileReader = file
   632  	} else {
   633  		fileReader = io.LimitReader(file, limit)
   634  	}
   635  
   636  	// Create a tomb to cancel watch events
   637  	waitCtx, cancel := context.WithCancel(ctx)
   638  	defer cancel()
   639  
   640  	// Create a variable to allow setting the last event
   641  	var lastEvent string
   642  
   643  	// Only create the file change watcher once. But we need to do it after we
   644  	// read and reach EOF.
   645  	var changes *watch.FileChanges
   646  
   647  	// Start streaming the data
   648  	bufSize := int64(streamFrameSize)
   649  	if limit > 0 && limit < streamFrameSize {
   650  		bufSize = limit
   651  	}
   652  	data := make([]byte, bufSize)
   653  OUTER:
   654  	for {
   655  		// Read up to the max frame size
   656  		n, readErr := fileReader.Read(data)
   657  
   658  		// Update the offset
   659  		offset += int64(n)
   660  
   661  		// Return non-EOF errors
   662  		if readErr != nil && readErr != io.EOF {
   663  			return readErr
   664  		}
   665  
   666  		// Send the frame
   667  		if n != 0 || lastEvent != "" {
   668  			if err := framer.Send(path, lastEvent, data[:n], offset); err != nil {
   669  				return parseFramerErr(err)
   670  			}
   671  		}
   672  
   673  		// Clear the last event
   674  		if lastEvent != "" {
   675  			lastEvent = ""
   676  		}
   677  
   678  		// Just keep reading since we aren't at the end of the file so we can
   679  		// avoid setting up a file event watcher.
   680  		if readErr == nil {
   681  			continue
   682  		}
   683  
   684  		// If EOF is hit, wait for a change to the file
   685  		if changes == nil {
   686  			changes, err = fs.ChangeEvents(waitCtx, path, offset)
   687  			if err != nil {
   688  				return err
   689  			}
   690  		}
   691  
   692  		for {
   693  			select {
   694  			case <-changes.Modified:
   695  				continue OUTER
   696  			case <-changes.Deleted:
   697  				return parseFramerErr(framer.Send(path, deleteEvent, nil, offset))
   698  			case <-changes.Truncated:
   699  				// Close the current reader
   700  				if err := file.Close(); err != nil {
   701  					return err
   702  				}
   703  
   704  				// Get a new reader at offset zero
   705  				offset = 0
   706  				var err error
   707  				file, err = fs.ReadAt(path, offset)
   708  				if err != nil {
   709  					return err
   710  				}
   711  				defer file.Close()
   712  
   713  				if limit <= 0 {
   714  					fileReader = file
   715  				} else {
   716  					// Get the current limit
   717  					lr, ok := fileReader.(*io.LimitedReader)
   718  					if !ok {
   719  						return fmt.Errorf("unable to determine remaining read limit")
   720  					}
   721  
   722  					fileReader = io.LimitReader(file, lr.N)
   723  				}
   724  
   725  				// Store the last event
   726  				lastEvent = truncateEvent
   727  				continue OUTER
   728  			case <-framer.ExitCh():
   729  				return nil
   730  			case <-ctx.Done():
   731  				return nil
   732  			case err, ok := <-eofCancelCh:
   733  				if !ok {
   734  					return nil
   735  				}
   736  
   737  				return err
   738  			}
   739  		}
   740  	}
   741  }
   742  
   743  // blockUntilNextLog returns a channel that will have data sent when the next
   744  // log index or anything greater is created.
   745  func blockUntilNextLog(ctx context.Context, fs allocdir.AllocDirFS, logPath, task, logType string, nextIndex int64) chan error {
   746  	nextPath := filepath.Join(logPath, fmt.Sprintf("%s.%s.%d", task, logType, nextIndex))
   747  	next := make(chan error, 1)
   748  
   749  	go func() {
   750  		eofCancelCh, err := fs.BlockUntilExists(ctx, nextPath)
   751  		if err != nil {
   752  			next <- err
   753  			close(next)
   754  			return
   755  		}
   756  
   757  		ticker := time.NewTicker(nextLogCheckRate)
   758  		defer ticker.Stop()
   759  		scanCh := ticker.C
   760  		for {
   761  			select {
   762  			case <-ctx.Done():
   763  				next <- nil
   764  				close(next)
   765  				return
   766  			case err := <-eofCancelCh:
   767  				next <- err
   768  				close(next)
   769  				return
   770  			case <-scanCh:
   771  				entries, err := fs.List(logPath)
   772  				if err != nil {
   773  					next <- fmt.Errorf("failed to list entries: %v", err)
   774  					close(next)
   775  					return
   776  				}
   777  
   778  				indexes, err := logIndexes(entries, task, logType)
   779  				if err != nil {
   780  					next <- err
   781  					close(next)
   782  					return
   783  				}
   784  
   785  				// Scan and see if there are any entries larger than what we are
   786  				// waiting for.
   787  				for _, entry := range indexes {
   788  					if entry.idx >= nextIndex {
   789  						next <- nil
   790  						close(next)
   791  						return
   792  					}
   793  				}
   794  			}
   795  		}
   796  	}()
   797  
   798  	return next
   799  }
   800  
   801  // indexTuple and indexTupleArray are used to find the correct log entry to
   802  // start streaming logs from
   803  type indexTuple struct {
   804  	idx   int64
   805  	entry *cstructs.AllocFileInfo
   806  }
   807  
   808  type indexTupleArray []indexTuple
   809  
   810  func (a indexTupleArray) Len() int           { return len(a) }
   811  func (a indexTupleArray) Less(i, j int) bool { return a[i].idx < a[j].idx }
   812  func (a indexTupleArray) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
   813  
   814  // logIndexes takes a set of entries and returns a indexTupleArray of
   815  // the desired log file entries. If the indexes could not be determined, an
   816  // error is returned.
   817  func logIndexes(entries []*cstructs.AllocFileInfo, task, logType string) (indexTupleArray, error) {
   818  	var indexes []indexTuple
   819  	prefix := fmt.Sprintf("%s.%s.", task, logType)
   820  	for _, entry := range entries {
   821  		if entry.IsDir {
   822  			continue
   823  		}
   824  
   825  		// If nothing was trimmed, then it is not a match
   826  		idxStr := strings.TrimPrefix(entry.Name, prefix)
   827  		if idxStr == entry.Name {
   828  			continue
   829  		}
   830  
   831  		// Convert to an int
   832  		idx, err := strconv.Atoi(idxStr)
   833  		if err != nil {
   834  			return nil, fmt.Errorf("failed to convert %q to a log index: %v", idxStr, err)
   835  		}
   836  
   837  		indexes = append(indexes, indexTuple{idx: int64(idx), entry: entry})
   838  	}
   839  
   840  	return indexTupleArray(indexes), nil
   841  }
   842  
   843  // notFoundErr is returned when a log is requested but cannot be found.
   844  // Implements agent.HTTPCodedError but does not reference it to avoid circular
   845  // imports.
   846  type notFoundErr struct {
   847  	taskName string
   848  	logType  string
   849  }
   850  
   851  func (e notFoundErr) Error() string {
   852  	return fmt.Sprintf("log entry for task %q and log type %q not found", e.taskName, e.logType)
   853  }
   854  
   855  // Code returns a 404 to avoid returning a 500
   856  func (e notFoundErr) Code() int {
   857  	return http.StatusNotFound
   858  }
   859  
   860  // findClosest takes a list of entries, the desired log index and desired log
   861  // offset (which can be negative, treated as offset from end), task name and log
   862  // type and returns the log entry, the log index, the offset to read from and a
   863  // potential error.
   864  func findClosest(entries []*cstructs.AllocFileInfo, desiredIdx, desiredOffset int64,
   865  	task, logType string) (*cstructs.AllocFileInfo, int64, int64, error) {
   866  
   867  	// Build the matching indexes
   868  	indexes, err := logIndexes(entries, task, logType)
   869  	if err != nil {
   870  		return nil, 0, 0, err
   871  	}
   872  	if len(indexes) == 0 {
   873  		return nil, 0, 0, notFoundErr{taskName: task, logType: logType}
   874  	}
   875  
   876  	// Binary search the indexes to get the desiredIdx
   877  	sort.Sort(indexes)
   878  	i := sort.Search(len(indexes), func(i int) bool { return indexes[i].idx >= desiredIdx })
   879  	l := len(indexes)
   880  	if i == l {
   881  		// Use the last index if the number is bigger than all of them.
   882  		i = l - 1
   883  	}
   884  
   885  	// Get to the correct offset
   886  	offset := desiredOffset
   887  	idx := int64(i)
   888  	for {
   889  		s := indexes[idx].entry.Size
   890  
   891  		// Base case
   892  		if offset == 0 {
   893  			break
   894  		} else if offset < 0 {
   895  			// Going backwards
   896  			if newOffset := s + offset; newOffset >= 0 {
   897  				// Current file works
   898  				offset = newOffset
   899  				break
   900  			} else if idx == 0 {
   901  				// Already at the end
   902  				offset = 0
   903  				break
   904  			} else {
   905  				// Try the file before
   906  				offset = newOffset
   907  				idx -= 1
   908  				continue
   909  			}
   910  		} else {
   911  			// Going forward
   912  			if offset <= s {
   913  				// Current file works
   914  				break
   915  			} else if idx == int64(l-1) {
   916  				// Already at the end
   917  				offset = s
   918  				break
   919  			} else {
   920  				// Try the next file
   921  				offset = offset - s
   922  				idx += 1
   923  				continue
   924  			}
   925  
   926  		}
   927  	}
   928  
   929  	return indexes[idx].entry, indexes[idx].idx, offset, nil
   930  }
   931  
   932  // parseFramerErr takes an error and returns an error. The error will
   933  // potentially change if it was caused by the connection being closed.
   934  func parseFramerErr(err error) error {
   935  	if err == nil {
   936  		return nil
   937  	}
   938  
   939  	errMsg := err.Error()
   940  
   941  	if strings.Contains(errMsg, io.ErrClosedPipe.Error()) {
   942  		// The pipe check is for tests
   943  		return syscall.EPIPE
   944  	}
   945  
   946  	// The connection was closed by our peer
   947  	if strings.Contains(errMsg, syscall.EPIPE.Error()) || strings.Contains(errMsg, syscall.ECONNRESET.Error()) {
   948  		return syscall.EPIPE
   949  	}
   950  
   951  	// Windows version of ECONNRESET
   952  	//XXX(schmichael) I could find no existing error or constant to
   953  	//                compare this against.
   954  	if strings.Contains(errMsg, "forcibly closed") {
   955  		return syscall.EPIPE
   956  	}
   957  
   958  	return err
   959  }