github.com/djenriquez/nomad-1@v0.8.1/client/fs_endpoint.go (about)

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