2022-06-14 23:17:36 +02:00
|
|
|
// Copyright 2022 Dashborg Inc
|
|
|
|
//
|
|
|
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
|
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
|
|
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
|
|
|
|
|
|
package cmdtail
|
|
|
|
|
|
|
|
import (
|
2022-06-15 07:16:58 +02:00
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"os"
|
2022-06-14 23:17:36 +02:00
|
|
|
"sync"
|
2022-06-15 07:16:58 +02:00
|
|
|
"time"
|
2022-06-14 23:17:36 +02:00
|
|
|
|
2022-06-15 07:16:58 +02:00
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/scripthaus-dev/sh2-runner/pkg/base"
|
2022-06-14 23:17:36 +02:00
|
|
|
"github.com/scripthaus-dev/sh2-runner/pkg/packet"
|
|
|
|
)
|
|
|
|
|
2022-06-15 07:16:58 +02:00
|
|
|
const MaxDataBytes = 4096
|
|
|
|
|
2022-06-14 23:17:36 +02:00
|
|
|
type TailPos struct {
|
2022-06-17 02:24:29 +02:00
|
|
|
ReqId string
|
2022-06-15 07:16:58 +02:00
|
|
|
Running bool // an active tailer sending data
|
|
|
|
TailPtyPos int64
|
|
|
|
TailRunPos int64
|
2022-06-16 01:29:39 +02:00
|
|
|
Follow bool
|
|
|
|
}
|
|
|
|
|
2022-06-17 02:24:29 +02:00
|
|
|
type CmdWatchEntry struct {
|
|
|
|
CmdKey CmdKey
|
|
|
|
FilePtyLen int64
|
|
|
|
FileRunLen int64
|
|
|
|
Tails []TailPos
|
|
|
|
}
|
|
|
|
|
|
|
|
func (w CmdWatchEntry) getTailPos(reqId string) (TailPos, bool) {
|
|
|
|
for _, pos := range w.Tails {
|
|
|
|
if pos.ReqId == reqId {
|
|
|
|
return pos, true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return TailPos{}, false
|
|
|
|
}
|
|
|
|
|
2022-06-17 07:23:29 +02:00
|
|
|
func (w *CmdWatchEntry) updateTailPos(reqId string, newPos TailPos) {
|
2022-06-17 02:24:29 +02:00
|
|
|
for idx, pos := range w.Tails {
|
|
|
|
if pos.ReqId == reqId {
|
2022-06-17 07:23:29 +02:00
|
|
|
w.Tails[idx] = newPos
|
2022-06-17 02:24:29 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
2022-06-17 07:23:29 +02:00
|
|
|
w.Tails = append(w.Tails, newPos)
|
2022-06-17 02:24:29 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func (w *CmdWatchEntry) removeTailPos(reqId string) {
|
|
|
|
var newTails []TailPos
|
|
|
|
for _, pos := range w.Tails {
|
|
|
|
if pos.ReqId == reqId {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
newTails = append(newTails, pos)
|
|
|
|
}
|
|
|
|
w.Tails = newTails
|
|
|
|
}
|
|
|
|
|
|
|
|
func (pos TailPos) IsCurrent(entry CmdWatchEntry) bool {
|
|
|
|
return pos.TailPtyPos >= entry.FilePtyLen && pos.TailRunPos >= entry.FileRunLen
|
2022-06-14 23:17:36 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
type CmdKey struct {
|
|
|
|
SessionId string
|
|
|
|
CmdId string
|
|
|
|
}
|
|
|
|
|
|
|
|
type Tailer struct {
|
|
|
|
Lock *sync.Mutex
|
2022-06-17 02:24:29 +02:00
|
|
|
WatchList map[CmdKey]CmdWatchEntry
|
2022-06-15 07:16:58 +02:00
|
|
|
ScHomeDir string
|
2022-06-16 01:29:39 +02:00
|
|
|
Watcher *SessionWatcher
|
2022-06-16 10:10:56 +02:00
|
|
|
SendCh chan packet.PacketType
|
2022-06-14 23:17:36 +02:00
|
|
|
}
|
|
|
|
|
2022-06-17 02:24:29 +02:00
|
|
|
func (t *Tailer) updateTailPos_nolock(cmdKey CmdKey, reqId string, pos TailPos) {
|
|
|
|
entry, found := t.WatchList[cmdKey]
|
|
|
|
if !found {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
entry.updateTailPos(reqId, pos)
|
|
|
|
t.WatchList[cmdKey] = entry
|
|
|
|
}
|
|
|
|
|
|
|
|
func (t *Tailer) updateEntrySizes_nolock(cmdKey CmdKey, ptyLen int64, runLen int64) {
|
|
|
|
entry, found := t.WatchList[cmdKey]
|
|
|
|
if !found {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
entry.FilePtyLen = ptyLen
|
|
|
|
entry.FileRunLen = runLen
|
|
|
|
t.WatchList[cmdKey] = entry
|
|
|
|
}
|
|
|
|
|
|
|
|
func (t *Tailer) getEntryAndPos_nolock(cmdKey CmdKey, reqId string) (CmdWatchEntry, TailPos, bool) {
|
|
|
|
entry, found := t.WatchList[cmdKey]
|
|
|
|
if !found {
|
|
|
|
return CmdWatchEntry{}, TailPos{}, false
|
|
|
|
}
|
|
|
|
pos, found := entry.getTailPos(reqId)
|
|
|
|
if !found {
|
|
|
|
return CmdWatchEntry{}, TailPos{}, false
|
|
|
|
}
|
|
|
|
return entry, pos, true
|
|
|
|
}
|
|
|
|
|
2022-06-16 10:10:56 +02:00
|
|
|
func MakeTailer(sendCh chan packet.PacketType) (*Tailer, error) {
|
2022-06-15 07:16:58 +02:00
|
|
|
scHomeDir, err := base.GetScHomeDir()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2022-06-14 23:17:36 +02:00
|
|
|
rtn := &Tailer{
|
|
|
|
Lock: &sync.Mutex{},
|
2022-06-17 02:24:29 +02:00
|
|
|
WatchList: make(map[CmdKey]CmdWatchEntry),
|
2022-06-15 07:16:58 +02:00
|
|
|
ScHomeDir: scHomeDir,
|
2022-06-16 10:10:56 +02:00
|
|
|
SendCh: sendCh,
|
2022-06-14 23:17:36 +02:00
|
|
|
}
|
2022-06-16 01:29:39 +02:00
|
|
|
rtn.Watcher, err = MakeSessionWatcher()
|
2022-06-14 23:17:36 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return rtn, nil
|
|
|
|
}
|
|
|
|
|
2022-06-15 07:16:58 +02:00
|
|
|
func (t *Tailer) readDataFromFile(fileName string, pos int64, maxBytes int) ([]byte, error) {
|
|
|
|
fd, err := os.Open(fileName)
|
|
|
|
defer fd.Close()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
buf := make([]byte, maxBytes)
|
|
|
|
nr, err := fd.ReadAt(buf, pos)
|
|
|
|
if err != nil && err != io.EOF { // ignore EOF error
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return buf[0:nr], nil
|
|
|
|
}
|
|
|
|
|
2022-06-17 02:24:29 +02:00
|
|
|
func (t *Tailer) makeCmdDataPacket(fileNames *base.CommandFileNames, entry CmdWatchEntry, pos TailPos) *packet.CmdDataPacketType {
|
2022-06-15 07:16:58 +02:00
|
|
|
dataPacket := packet.MakeCmdDataPacket()
|
2022-06-17 02:24:29 +02:00
|
|
|
dataPacket.ReqId = pos.ReqId
|
|
|
|
dataPacket.SessionId = entry.CmdKey.SessionId
|
|
|
|
dataPacket.CmdId = entry.CmdKey.CmdId
|
2022-06-15 07:16:58 +02:00
|
|
|
dataPacket.PtyPos = pos.TailPtyPos
|
|
|
|
dataPacket.RunPos = pos.TailRunPos
|
2022-06-17 02:24:29 +02:00
|
|
|
if entry.FilePtyLen > pos.TailPtyPos {
|
2022-06-15 07:16:58 +02:00
|
|
|
ptyData, err := t.readDataFromFile(fileNames.PtyOutFile, pos.TailPtyPos, MaxDataBytes)
|
|
|
|
if err != nil {
|
|
|
|
dataPacket.Error = err.Error()
|
|
|
|
return dataPacket
|
|
|
|
}
|
|
|
|
dataPacket.PtyData = string(ptyData)
|
2022-06-17 02:24:29 +02:00
|
|
|
dataPacket.PtyDataLen = len(ptyData)
|
2022-06-15 07:16:58 +02:00
|
|
|
}
|
2022-06-17 02:24:29 +02:00
|
|
|
if entry.FileRunLen > pos.TailRunPos {
|
2022-06-15 07:16:58 +02:00
|
|
|
runData, err := t.readDataFromFile(fileNames.RunnerOutFile, pos.TailRunPos, MaxDataBytes)
|
|
|
|
if err != nil {
|
|
|
|
dataPacket.Error = err.Error()
|
|
|
|
return dataPacket
|
|
|
|
}
|
|
|
|
dataPacket.RunData = string(runData)
|
2022-06-17 02:24:29 +02:00
|
|
|
dataPacket.RunDataLen = len(runData)
|
2022-06-15 07:16:58 +02:00
|
|
|
}
|
|
|
|
return dataPacket
|
|
|
|
}
|
|
|
|
|
|
|
|
// returns (data-packet, keepRunning)
|
2022-06-17 02:24:29 +02:00
|
|
|
func (t *Tailer) runSingleDataTransfer(key CmdKey, reqId string) (*packet.CmdDataPacketType, bool) {
|
2022-06-15 07:16:58 +02:00
|
|
|
t.Lock.Lock()
|
2022-06-17 02:24:29 +02:00
|
|
|
entry, pos, foundPos := t.getEntryAndPos_nolock(key, reqId)
|
2022-06-15 07:16:58 +02:00
|
|
|
t.Lock.Unlock()
|
|
|
|
if !foundPos {
|
|
|
|
return nil, false
|
|
|
|
}
|
|
|
|
fileNames := base.MakeCommandFileNamesWithHome(t.ScHomeDir, key.SessionId, key.CmdId)
|
2022-06-17 02:24:29 +02:00
|
|
|
dataPacket := t.makeCmdDataPacket(fileNames, entry, pos)
|
2022-06-15 07:16:58 +02:00
|
|
|
|
|
|
|
t.Lock.Lock()
|
|
|
|
defer t.Lock.Unlock()
|
2022-06-17 02:24:29 +02:00
|
|
|
entry, pos, foundPos = t.getEntryAndPos_nolock(key, reqId)
|
2022-06-15 07:16:58 +02:00
|
|
|
if !foundPos {
|
|
|
|
return nil, false
|
|
|
|
}
|
|
|
|
// pos was updated between first and second get, throw out data-packet and re-run
|
|
|
|
if pos.TailPtyPos != dataPacket.PtyPos || pos.TailRunPos != dataPacket.RunPos {
|
|
|
|
return nil, true
|
|
|
|
}
|
|
|
|
if dataPacket.Error != "" {
|
|
|
|
// error, so return error packet, and stop running
|
|
|
|
pos.Running = false
|
2022-06-17 02:24:29 +02:00
|
|
|
t.updateTailPos_nolock(key, reqId, pos)
|
2022-06-15 07:16:58 +02:00
|
|
|
return dataPacket, false
|
|
|
|
}
|
|
|
|
pos.TailPtyPos += int64(len(dataPacket.PtyData))
|
|
|
|
pos.TailRunPos += int64(len(dataPacket.RunData))
|
2022-06-17 02:24:29 +02:00
|
|
|
if pos.TailPtyPos >= entry.FilePtyLen && pos.TailRunPos >= entry.FileRunLen {
|
2022-06-15 07:16:58 +02:00
|
|
|
// we caught up, tail position equals file length
|
|
|
|
pos.Running = false
|
|
|
|
}
|
2022-06-17 02:24:29 +02:00
|
|
|
t.updateTailPos_nolock(key, reqId, pos)
|
2022-06-15 07:16:58 +02:00
|
|
|
return dataPacket, pos.Running
|
|
|
|
}
|
|
|
|
|
2022-06-17 02:24:29 +02:00
|
|
|
func (t *Tailer) checkRemoveNoFollow(cmdKey CmdKey, reqId string) {
|
2022-06-16 01:29:39 +02:00
|
|
|
t.Lock.Lock()
|
|
|
|
defer t.Lock.Unlock()
|
2022-06-17 02:24:29 +02:00
|
|
|
entry, pos, foundPos := t.getEntryAndPos_nolock(cmdKey, reqId)
|
2022-06-16 01:29:39 +02:00
|
|
|
if !foundPos {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if !pos.Follow {
|
2022-06-17 02:24:29 +02:00
|
|
|
entry.removeTailPos(reqId)
|
|
|
|
if len(entry.Tails) == 0 {
|
|
|
|
delete(t.WatchList, cmdKey)
|
|
|
|
} else {
|
|
|
|
t.WatchList[cmdKey] = entry
|
|
|
|
}
|
2022-06-16 01:29:39 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-17 02:24:29 +02:00
|
|
|
func (t *Tailer) RunDataTransfer(key CmdKey, reqId string) {
|
2022-06-15 07:16:58 +02:00
|
|
|
for {
|
2022-06-17 02:24:29 +02:00
|
|
|
dataPacket, keepRunning := t.runSingleDataTransfer(key, reqId)
|
2022-06-15 07:16:58 +02:00
|
|
|
if dataPacket != nil {
|
2022-06-16 10:10:56 +02:00
|
|
|
t.SendCh <- dataPacket
|
2022-06-15 07:16:58 +02:00
|
|
|
}
|
|
|
|
if !keepRunning {
|
2022-06-17 02:24:29 +02:00
|
|
|
t.checkRemoveNoFollow(key, reqId)
|
2022-06-15 07:16:58 +02:00
|
|
|
break
|
|
|
|
}
|
|
|
|
time.Sleep(10 * time.Millisecond)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-16 01:29:39 +02:00
|
|
|
// should already hold t.Lock
|
2022-06-17 02:24:29 +02:00
|
|
|
func (t *Tailer) tryStartRun_nolock(entry CmdWatchEntry, pos TailPos) {
|
|
|
|
if pos.Running || pos.IsCurrent(entry) {
|
2022-06-15 07:16:58 +02:00
|
|
|
return
|
|
|
|
}
|
2022-06-16 01:29:39 +02:00
|
|
|
pos.Running = true
|
2022-06-17 02:24:29 +02:00
|
|
|
t.updateTailPos_nolock(entry.CmdKey, pos.ReqId, pos)
|
|
|
|
go t.RunDataTransfer(entry.CmdKey, pos.ReqId)
|
2022-06-16 01:29:39 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func (t *Tailer) updateFile(event FileUpdateEvent) {
|
|
|
|
if event.Err != nil {
|
2022-06-16 10:10:56 +02:00
|
|
|
t.SendCh <- packet.FmtMessagePacket("error in FileUpdateEvent %s/%s: %v", event.SessionId, event.CmdId, event.Err)
|
2022-06-15 07:16:58 +02:00
|
|
|
return
|
|
|
|
}
|
2022-06-16 01:29:39 +02:00
|
|
|
cmdKey := CmdKey{SessionId: event.SessionId, CmdId: event.CmdId}
|
2022-06-15 07:16:58 +02:00
|
|
|
t.Lock.Lock()
|
|
|
|
defer t.Lock.Unlock()
|
2022-06-17 02:24:29 +02:00
|
|
|
entry, foundEntry := t.WatchList[cmdKey]
|
|
|
|
if !foundEntry {
|
2022-06-15 07:16:58 +02:00
|
|
|
return
|
|
|
|
}
|
2022-06-16 01:29:39 +02:00
|
|
|
if event.FileType == FileTypePty {
|
2022-06-17 02:24:29 +02:00
|
|
|
entry.FilePtyLen = event.Size
|
2022-06-16 01:29:39 +02:00
|
|
|
} else if event.FileType == FileTypeRun {
|
2022-06-17 02:24:29 +02:00
|
|
|
entry.FileRunLen = event.Size
|
|
|
|
}
|
|
|
|
t.WatchList[cmdKey] = entry
|
|
|
|
for _, pos := range entry.Tails {
|
|
|
|
t.tryStartRun_nolock(entry, pos)
|
2022-06-15 07:16:58 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-16 01:29:39 +02:00
|
|
|
func (t *Tailer) Run() error {
|
|
|
|
go func() {
|
|
|
|
for event := range t.Watcher.EventCh {
|
|
|
|
t.updateFile(event)
|
2022-06-15 07:16:58 +02:00
|
|
|
}
|
2022-06-16 01:29:39 +02:00
|
|
|
}()
|
|
|
|
err := t.Watcher.Run(nil)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2022-06-17 07:23:29 +02:00
|
|
|
func (t *Tailer) Close() error {
|
|
|
|
return t.Watcher.Close()
|
|
|
|
}
|
|
|
|
|
2022-06-16 01:29:39 +02:00
|
|
|
func max(v1 int64, v2 int64) int64 {
|
|
|
|
if v1 > v2 {
|
|
|
|
return v1
|
2022-06-15 07:16:58 +02:00
|
|
|
}
|
2022-06-16 01:29:39 +02:00
|
|
|
return v2
|
2022-06-15 07:16:58 +02:00
|
|
|
}
|
|
|
|
|
2022-06-17 02:24:29 +02:00
|
|
|
func (entry *CmdWatchEntry) fillFilePos(scHomeDir string) {
|
|
|
|
fileNames := base.MakeCommandFileNamesWithHome(scHomeDir, entry.CmdKey.SessionId, entry.CmdKey.CmdId)
|
2022-06-15 07:16:58 +02:00
|
|
|
ptyInfo, _ := os.Stat(fileNames.PtyOutFile)
|
|
|
|
if ptyInfo != nil {
|
2022-06-17 02:24:29 +02:00
|
|
|
entry.FilePtyLen = ptyInfo.Size()
|
2022-06-16 01:29:39 +02:00
|
|
|
}
|
2022-06-15 07:16:58 +02:00
|
|
|
runoutInfo, _ := os.Stat(fileNames.RunnerOutFile)
|
|
|
|
if runoutInfo != nil {
|
2022-06-17 02:24:29 +02:00
|
|
|
entry.FileRunLen = runoutInfo.Size()
|
2022-06-16 01:29:39 +02:00
|
|
|
}
|
2022-06-15 07:16:58 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
func (t *Tailer) AddWatch(getPacket *packet.GetCmdPacketType) error {
|
|
|
|
_, err := uuid.Parse(getPacket.SessionId)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("getcmd, bad sessionid '%s': %w", getPacket.SessionId, err)
|
|
|
|
}
|
|
|
|
_, err = uuid.Parse(getPacket.CmdId)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("getcmd, bad cmdid '%s': %w", getPacket.CmdId, err)
|
|
|
|
}
|
2022-06-17 02:24:29 +02:00
|
|
|
if getPacket.ReqId == "" {
|
|
|
|
return fmt.Errorf("getcmd, no reqid specified")
|
|
|
|
}
|
2022-06-15 07:16:58 +02:00
|
|
|
t.Lock.Lock()
|
|
|
|
defer t.Lock.Unlock()
|
|
|
|
key := CmdKey{getPacket.SessionId, getPacket.CmdId}
|
2022-06-16 01:29:39 +02:00
|
|
|
err = t.Watcher.WatchSession(getPacket.SessionId)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("error trying to watch sesion '%s': %v", getPacket.SessionId, err)
|
2022-06-15 07:16:58 +02:00
|
|
|
}
|
2022-06-17 02:24:29 +02:00
|
|
|
entry, foundEntry := t.WatchList[key]
|
|
|
|
if !foundEntry {
|
|
|
|
entry = CmdWatchEntry{CmdKey: key}
|
|
|
|
entry.fillFilePos(t.ScHomeDir)
|
|
|
|
}
|
|
|
|
pos := TailPos{ReqId: getPacket.ReqId, TailPtyPos: getPacket.PtyPos, TailRunPos: getPacket.RunPos, Follow: getPacket.Tail}
|
|
|
|
// convert negative pos to positive
|
|
|
|
if pos.TailPtyPos < 0 {
|
|
|
|
pos.TailPtyPos = max(0, entry.FilePtyLen+pos.TailPtyPos) // + because negative
|
|
|
|
}
|
|
|
|
if pos.TailRunPos < 0 {
|
|
|
|
pos.TailRunPos = max(0, entry.FileRunLen+pos.TailRunPos) // + because negative
|
|
|
|
}
|
|
|
|
entry.updateTailPos(pos.ReqId, pos)
|
|
|
|
t.WatchList[key] = entry
|
|
|
|
t.tryStartRun_nolock(entry, pos)
|
2022-06-14 23:17:36 +02:00
|
|
|
return nil
|
|
|
|
}
|