Skip to content

Commit

Permalink
CW Issue #1039: Accept a projectCreationTime on WS API and pass that …
Browse files Browse the repository at this point in the history
…to cwctl, rather than 0. (#35)
  • Loading branch information
jgwest authored Nov 18, 2019
1 parent 32f0267 commit d82b7d0
Show file tree
Hide file tree
Showing 12 changed files with 407 additions and 103 deletions.
25 changes: 15 additions & 10 deletions Filewatcherd-Go/src/codewind/clistate.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,7 @@ import (
"time"
)

// CLIState ...
//
// The purpose of this is to call the cwctl project sync command, in order to allow the
// CLIState will call the cwctl project sync command, in order to allow the
// Codewind CLI to detect and communicate file changes to the server.
//
// This class will ensure that only one instance of the cwctl project sync command is running
Expand Down Expand Up @@ -67,10 +65,10 @@ func NewCLIState(projectIDParam string, installerPathParam string, projectPathPa

}

// OnFileChangeEvent is called by eventbatchutil and projectlist.
// OnFileChangeEvent is called by eventbatchutil and projectlist.
// This method is defacto non-blocking: it will pass the file notification to the go channel (which should be read immediately)
// then immediately return.
func (state *CLIState) OnFileChangeEvent() error {
func (state *CLIState) OnFileChangeEvent(projectCreationTimeInAbsoluteMsecsParam int64) error {

if strings.TrimSpace(state.projectPath) == "" {
msg := "Project path passed to CLIState is empty, so ignoring file change event."
Expand All @@ -79,7 +77,7 @@ func (state *CLIState) OnFileChangeEvent() error {
}

// Inform channel that a new file change list was received (but don't actually send it)
state.channel <- CLIStateChannelEntry{nil}
state.channel <- CLIStateChannelEntry{projectCreationTimeInAbsoluteMsecsParam, nil}

return nil
}
Expand All @@ -101,7 +99,7 @@ func (state *CLIState) readChannel() {
rpr := channelResult.runProjectReturn

if rpr.errorCode == 0 {
// Success, so update the tiemstamp to the process start time.
// Success, so update the timestamp to the process start time.
lastTimestamp = rpr.spawnTime
utils.LogInfo("Updating timestamp to latest: " + strconv.FormatInt(lastTimestamp, 10))

Expand All @@ -110,6 +108,12 @@ func (state *CLIState) readChannel() {
}

} else {

if channelResult.projectCreationTimeInAbsoluteMsecsParam != 0 && lastTimestamp == 0 {
utils.LogInfo("Timestamp updated from " + timestampToString(lastTimestamp) + " to " + timestampToString(channelResult.projectCreationTimeInAbsoluteMsecsParam) + " from project creation time.")
lastTimestamp = channelResult.projectCreationTimeInAbsoluteMsecsParam
}

// Another thread has informed us of new file changes
processWaiting = true
}
Expand All @@ -126,7 +130,8 @@ func (state *CLIState) readChannel() {

// CLIStateChannelEntry runprojectReturn will be non-null if it is a runProjectCommand response, otherwise null if it is a new file change. */
type CLIStateChannelEntry struct {
runProjectReturn *RunProjectReturn
projectCreationTimeInAbsoluteMsecsParam int64
runProjectReturn *RunProjectReturn
}

func (state *CLIState) runProjectCommand(timestamp int64) {
Expand Down Expand Up @@ -202,7 +207,7 @@ func (state *CLIState) runProjectCommand(timestamp int64) {
spawnTimeInMsecs,
}

state.channel <- CLIStateChannelEntry{&result}
state.channel <- CLIStateChannelEntry{0, &result}

} else {

Expand All @@ -215,7 +220,7 @@ func (state *CLIState) runProjectCommand(timestamp int64) {
spawnTimeInMsecs,
}

state.channel <- CLIStateChannelEntry{&result}
state.channel <- CLIStateChannelEntry{0, &result}

}
}
Expand Down
22 changes: 10 additions & 12 deletions Filewatcherd-Go/src/codewind/eventbatchutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,18 +52,18 @@ import (
*/
type FileChangeEventBatchUtil struct {
filesChangesChan chan []ChangedFileEntry
debugState_synch_lock string // Lock 'lock' before reading/writing this
cliState *CLIState // nullable
debugState_synch_lock string // Lock 'lock' before reading/writing this
projectList *ProjectList
lock *sync.Mutex
}

func NewFileChangeEventBatchUtil(projectID string, postOutputQueue *HttpPostOutputQueue, cliStateParam *CLIState) *FileChangeEventBatchUtil {
func NewFileChangeEventBatchUtil(projectID string, postOutputQueue *HttpPostOutputQueue, projectList *ProjectList) *FileChangeEventBatchUtil {

result := &FileChangeEventBatchUtil{
filesChangesChan: make(chan []ChangedFileEntry),
debugState_synch_lock: "",
lock: &sync.Mutex{},
cliState: cliStateParam,
projectList: projectList,
}

go result.fileChangeListener(projectID, postOutputQueue)
Expand Down Expand Up @@ -110,7 +110,7 @@ func (e *FileChangeEventBatchUtil) fileChangeListener(projectID string, postOutp
if timer1 != nil && timer1 == timerReceived {

if len(eventsReceivedSinceLastBatch) > 0 {
processAndSendEvents(eventsReceivedSinceLastBatch, projectID, postOutputQueue, e.cliState)
processAndSendEvents(eventsReceivedSinceLastBatch, projectID, postOutputQueue, e.projectList)
}
eventsReceivedSinceLastBatch = []ChangedFileEntry{}
timer1 = nil
Expand Down Expand Up @@ -148,7 +148,7 @@ func (e *FileChangeEventBatchUtil) updateDebugState(debugTimeSinceLastFileChange
}

/** Process the event list, split it into chunks, then pass it to the HTTP POST output queue */
func processAndSendEvents(eventsToSend []ChangedFileEntry, projectID string, postOutputQueue *HttpPostOutputQueue, cliState *CLIState) {
func processAndSendEvents(eventsToSend []ChangedFileEntry, projectID string, postOutputQueue *HttpPostOutputQueue, projectList *ProjectList) {
sort.SliceStable(eventsToSend, func(i, j int) bool {

// Sort ascending by timestamp
Expand All @@ -172,15 +172,13 @@ func processAndSendEvents(eventsToSend []ChangedFileEntry, projectID string, pos
utils.LogInfo(
"Batch change summary for " + projectID + "@ " + strconv.FormatInt(mostRecentTimestamp.timestamp, 10) + ": " + changeSummary)

if cliState != nil {
// Inform CLI of changes
cliState.OnFileChangeEvent()
// Inform CLI of changes
projectList.CLIFileChangeUpdate(projectID)

} else {
// TODO: Remove this entire if block once CWCTL sync is mature.
if false {
// Use the old way of communicating file changes via POST packets.

// TODO: Remove this entire else block once CWCTL sync is mature.

var fileListsToSend [][]changedFileEntryJSON

for len(eventsToSend) > 0 {
Expand Down
23 changes: 15 additions & 8 deletions Filewatcherd-Go/src/codewind/models/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,19 @@

package models

// ProjectToWatch ...
type ProjectToWatch struct {
IgnoredFilenames []string `json:"ignoredFilenames"`
IgnoredPaths []string `json:"ignoredPaths"`
PathToMonitor string `json:"pathToMonitor"`
ProjectID string `json:"projectID"`
ChangeType string `json:"changeType"`
ProjectWatchStateID string `json:"projectWatchStateId"`
Type string `json:"type"`
IgnoredFilenames []string `json:"ignoredFilenames"`
IgnoredPaths []string `json:"ignoredPaths"`
PathToMonitor string `json:"pathToMonitor"`
ProjectID string `json:"projectID"`
ChangeType string `json:"changeType"`
ProjectWatchStateID string `json:"projectWatchStateId"`
Type string `json:"type"`
ProjectCreationTime int64 `json:"projectCreationTime"`
}

/** This is not currently used, but I reserve the right to clone all the things at a later date. */
// Clone performs a deep copy of a ProjectToWatch
func (entry *ProjectToWatch) Clone() *ProjectToWatch {

var newIgnoredFilenames []string
Expand Down Expand Up @@ -50,21 +52,26 @@ func (entry *ProjectToWatch) Clone() *ProjectToWatch {
entry.ChangeType,
entry.ProjectWatchStateID,
entry.Type,
entry.ProjectCreationTime,
}
}

// WatchlistEntries ...
type WatchlistEntries []ProjectToWatch

// WatchlistEntryList ...
type WatchlistEntryList struct {
Projects WatchlistEntries `json:"projects"`
}

// WatchEventEntry ...
type WatchEventEntry struct {
EventType string
Path string
IsDir bool
}

// WatchChangeJson ...
type WatchChangeJson struct {
Type string `json:"type"`
Projects WatchlistEntries `json:"projects"`
Expand Down
116 changes: 98 additions & 18 deletions Filewatcherd-Go/src/codewind/projectlist.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,19 @@ package main
import (
"codewind/models"
"codewind/utils"
"strconv"
"strings"
"time"
)

/**
* ProjectList is the API entrypoint for other code in this application to perform operations against monitored projects:
* - Update project list from a GET response
* - Update project list from a WebSocket response
* - Process a file update and pass it to batch utility
*
* Behind the scenes, the ProjectList API calls are translated into channel messages and placed on the projectOperationChannel.
* This allows us to provide thread safety to the internal project list data, as that data will only ever be accessed
* by a single goroutine.
*/
// ProjectList is the API entrypoint for other code in this application to perform operations against monitored projects:
// - Update project list from a GET response
// - Update project list from a WebSocket response
// - Process a file update and pass it to batch utility
//
// Behind the scenes, the ProjectList API calls are translated into channel messages and placed on the projectOperationChannel.
// This allows us to provide thread safety to the internal project list data, as that data will only ever be accessed
// by a single goroutine.
type ProjectList struct {
projectOperationChannel chan *projectListChannelMessage
pathToInstaller string // nullable
Expand All @@ -38,6 +37,7 @@ type receiveNewWatchEntriesMessage struct {
project *models.ProjectToWatch
}

// NewProjectList ...
func NewProjectList(postOutputQueue *HttpPostOutputQueue, pathToInstallerParam string) *ProjectList {

result := &ProjectList{}
Expand Down Expand Up @@ -159,7 +159,7 @@ func (projectList *ProjectList) channelListener(postOutputQueue *HttpPostOutputQ
}
}

/** Generate an overview of the state of the project list, including the projects being watched. */
/** Inform the CLI of a file change on the specified project. */
func (projectList *ProjectList) handleCliFileChangeUpdate(projectID string, projectsMap map[string]*projectObject) {

value, exists := projectsMap[projectID]
Expand All @@ -175,7 +175,9 @@ func (projectList *ProjectList) handleCliFileChangeUpdate(projectID string, proj
return
}

value.cliState.OnFileChangeEvent()
if value.cliState != nil {
value.cliState.OnFileChangeEvent(value.project.ProjectCreationTime)
}

}

Expand Down Expand Up @@ -311,18 +313,92 @@ func (projectList *ProjectList) handleUpdateProjectListFromWebSocket(webSocketUp

}

/**
* Synchronize the project in our projectsMap (if it exists), with the new 'projectToProcess' from the server.
* If it doesn't exist, create it.*/
// Synchronize the project in our projectsMap (if it exists), with the new 'projectToProcess' from the server.
// If it doesn't exist, create it.
func (projectList *ProjectList) processProject(projectToProcess models.ProjectToWatch, projectsMap map[string]*projectObject, postOutputQueue *HttpPostOutputQueue, watchService *WatchService) {

currProjWatchState, exists := projectsMap[projectToProcess.ProjectID]
if exists {
// If we have previously monitored this project...

if currProjWatchState.project.PathToMonitor == projectToProcess.PathToMonitor {
oldProjectToWatch := currProjWatchState.project

// This method may receive ProjectToWatch objects with either null or non-null
// values for the `projectCreationTimeInAbsoluteMsecs` field. However, under no
// circumstances should we ever replace a non-null value for this field with a
// null field.
//
// For this reason, we carefully compare these values in this if block and
// update accordingly.
{
pctUpdated := false

pctOldProjectToWatch := oldProjectToWatch.ProjectCreationTime
pctNewProjectToWatch := projectToProcess.ProjectCreationTime

newPct := int64(0)

// If both the old and new values are not null, but the value has changed, then
// use the new value.
if pctNewProjectToWatch != 0 && pctOldProjectToWatch != 0 && pctNewProjectToWatch != pctOldProjectToWatch {

newPct = pctNewProjectToWatch

utils.LogInfo("The project creation time has changed, when both values were non-null. Old: " + timestampToString(pctOldProjectToWatch) + " New: " + timestampToString(pctNewProjectToWatch) + " for project " + projectToProcess.ProjectID)

pctUpdated = true
}

// If old is not-null, and new is null, then DON'T overwrite the old one with
// the new one.
if pctOldProjectToWatch != 0 && pctNewProjectToWatch == 0 {

newPct = pctOldProjectToWatch

utils.LogInfo(
"Internal project creation state was preserved, despite receiving a project update w/o this value. Current: " + timestampToString(pctOldProjectToWatch) + " Received: " + timestampToString(pctNewProjectToWatch) + " for project " + projectToProcess.ProjectID)

newPtw := *(projectToProcess.Clone())
newPtw.ProjectCreationTime = newPct

if newPtw.ProjectCreationTime != pctOldProjectToWatch {
utils.LogSevere("Updated PTW field did not have correct projectCreationTime, for project " + projectToProcess.ProjectID)
}

// Update the ptw, in case it is used by the following if block, but DONT call
// po.updatePTW(...) with it.
projectToProcess = newPtw
pctUpdated = false // this is false so that updatePTW(...) is not called.
}

// If the old is null, and the new is not null, then overwrite the old with the
// new.
if pctOldProjectToWatch == 0 && pctNewProjectToWatch != 0 {

newPct = pctNewProjectToWatch

utils.LogInfo("The project creation time has changed. Old: " + timestampToString(pctOldProjectToWatch) + " New: " + timestampToString(pctNewProjectToWatch) + ", for project " + projectToProcess.ProjectID)

pctUpdated = true

}

if pctUpdated {

oldProjectToWatch := currProjWatchState.project
newPtw := *(projectToProcess.Clone())
newPtw.ProjectCreationTime = newPct

// Update the object itself, in case the if-branch below this one is executed.
projectToProcess = newPtw

// This logic may cause the PO to be updated twice (once here, and once below,
// but this is fine)
currProjWatchState.project = &projectToProcess
}

}

if currProjWatchState.project.PathToMonitor == projectToProcess.PathToMonitor {

fileToMonitor, err := utils.ConvertAbsoluteUnixStyleNormalizedPathToLocalFile(projectToProcess.PathToMonitor)
if err != nil {
Expand Down Expand Up @@ -383,6 +459,10 @@ func (projectList *ProjectList) processProject(projectToProcess models.ProjectTo

}

func timestampToString(ts int64) string {
return strconv.FormatInt(ts, 10)
}

/** This function is called with a new file change entry, which is filtered (if necessary) then patched to the project's batch utility object. */
func handleReceiveNewWatchEventEntries(projectMatch *models.ProjectToWatch, entry *models.WatchEventEntry, projectsMap map[string]*projectObject) {

Expand Down Expand Up @@ -479,7 +559,7 @@ func (projectList *ProjectList) newProjectObject(project models.ProjectToWatch,

return &projectObject{
&project,
NewFileChangeEventBatchUtil(project.ProjectID, postOutputQueue, cliState),
NewFileChangeEventBatchUtil(project.ProjectID, postOutputQueue, projectList),
cliState, // May be null
}, nil
}
Loading

0 comments on commit d82b7d0

Please sign in to comment.