Skip to content

Commit d82b7d0

Browse files
authored
CW Issue #1039: Accept a projectCreationTime on WS API and pass that to cwctl, rather than 0. (#35)
1 parent 32f0267 commit d82b7d0

File tree

12 files changed

+407
-103
lines changed

12 files changed

+407
-103
lines changed

Filewatcherd-Go/src/codewind/clistate.go

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,7 @@ import (
2222
"time"
2323
)
2424

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

6866
}
6967

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

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

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

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

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

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

112110
} else {
111+
112+
if channelResult.projectCreationTimeInAbsoluteMsecsParam != 0 && lastTimestamp == 0 {
113+
utils.LogInfo("Timestamp updated from " + timestampToString(lastTimestamp) + " to " + timestampToString(channelResult.projectCreationTimeInAbsoluteMsecsParam) + " from project creation time.")
114+
lastTimestamp = channelResult.projectCreationTimeInAbsoluteMsecsParam
115+
}
116+
113117
// Another thread has informed us of new file changes
114118
processWaiting = true
115119
}
@@ -126,7 +130,8 @@ func (state *CLIState) readChannel() {
126130

127131
// CLIStateChannelEntry runprojectReturn will be non-null if it is a runProjectCommand response, otherwise null if it is a new file change. */
128132
type CLIStateChannelEntry struct {
129-
runProjectReturn *RunProjectReturn
133+
projectCreationTimeInAbsoluteMsecsParam int64
134+
runProjectReturn *RunProjectReturn
130135
}
131136

132137
func (state *CLIState) runProjectCommand(timestamp int64) {
@@ -202,7 +207,7 @@ func (state *CLIState) runProjectCommand(timestamp int64) {
202207
spawnTimeInMsecs,
203208
}
204209

205-
state.channel <- CLIStateChannelEntry{&result}
210+
state.channel <- CLIStateChannelEntry{0, &result}
206211

207212
} else {
208213

@@ -215,7 +220,7 @@ func (state *CLIState) runProjectCommand(timestamp int64) {
215220
spawnTimeInMsecs,
216221
}
217222

218-
state.channel <- CLIStateChannelEntry{&result}
223+
state.channel <- CLIStateChannelEntry{0, &result}
219224

220225
}
221226
}

Filewatcherd-Go/src/codewind/eventbatchutil.go

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -52,18 +52,18 @@ import (
5252
*/
5353
type FileChangeEventBatchUtil struct {
5454
filesChangesChan chan []ChangedFileEntry
55-
debugState_synch_lock string // Lock 'lock' before reading/writing this
56-
cliState *CLIState // nullable
55+
debugState_synch_lock string // Lock 'lock' before reading/writing this
56+
projectList *ProjectList
5757
lock *sync.Mutex
5858
}
5959

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

6262
result := &FileChangeEventBatchUtil{
6363
filesChangesChan: make(chan []ChangedFileEntry),
6464
debugState_synch_lock: "",
6565
lock: &sync.Mutex{},
66-
cliState: cliStateParam,
66+
projectList: projectList,
6767
}
6868

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

112112
if len(eventsReceivedSinceLastBatch) > 0 {
113-
processAndSendEvents(eventsReceivedSinceLastBatch, projectID, postOutputQueue, e.cliState)
113+
processAndSendEvents(eventsReceivedSinceLastBatch, projectID, postOutputQueue, e.projectList)
114114
}
115115
eventsReceivedSinceLastBatch = []ChangedFileEntry{}
116116
timer1 = nil
@@ -148,7 +148,7 @@ func (e *FileChangeEventBatchUtil) updateDebugState(debugTimeSinceLastFileChange
148148
}
149149

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

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

175-
if cliState != nil {
176-
// Inform CLI of changes
177-
cliState.OnFileChangeEvent()
175+
// Inform CLI of changes
176+
projectList.CLIFileChangeUpdate(projectID)
178177

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

182-
// TODO: Remove this entire else block once CWCTL sync is mature.
183-
184182
var fileListsToSend [][]changedFileEntryJSON
185183

186184
for len(eventsToSend) > 0 {

Filewatcherd-Go/src/codewind/models/models.go

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,19 @@
1111

1212
package models
1313

14+
// ProjectToWatch ...
1415
type ProjectToWatch struct {
15-
IgnoredFilenames []string `json:"ignoredFilenames"`
16-
IgnoredPaths []string `json:"ignoredPaths"`
17-
PathToMonitor string `json:"pathToMonitor"`
18-
ProjectID string `json:"projectID"`
19-
ChangeType string `json:"changeType"`
20-
ProjectWatchStateID string `json:"projectWatchStateId"`
21-
Type string `json:"type"`
16+
IgnoredFilenames []string `json:"ignoredFilenames"`
17+
IgnoredPaths []string `json:"ignoredPaths"`
18+
PathToMonitor string `json:"pathToMonitor"`
19+
ProjectID string `json:"projectID"`
20+
ChangeType string `json:"changeType"`
21+
ProjectWatchStateID string `json:"projectWatchStateId"`
22+
Type string `json:"type"`
23+
ProjectCreationTime int64 `json:"projectCreationTime"`
2224
}
2325

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

2729
var newIgnoredFilenames []string
@@ -50,21 +52,26 @@ func (entry *ProjectToWatch) Clone() *ProjectToWatch {
5052
entry.ChangeType,
5153
entry.ProjectWatchStateID,
5254
entry.Type,
55+
entry.ProjectCreationTime,
5356
}
5457
}
5558

59+
// WatchlistEntries ...
5660
type WatchlistEntries []ProjectToWatch
5761

62+
// WatchlistEntryList ...
5863
type WatchlistEntryList struct {
5964
Projects WatchlistEntries `json:"projects"`
6065
}
6166

67+
// WatchEventEntry ...
6268
type WatchEventEntry struct {
6369
EventType string
6470
Path string
6571
IsDir bool
6672
}
6773

74+
// WatchChangeJson ...
6875
type WatchChangeJson struct {
6976
Type string `json:"type"`
7077
Projects WatchlistEntries `json:"projects"`

Filewatcherd-Go/src/codewind/projectlist.go

Lines changed: 98 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,19 @@ package main
1414
import (
1515
"codewind/models"
1616
"codewind/utils"
17+
"strconv"
1718
"strings"
1819
"time"
1920
)
2021

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

40+
// NewProjectList ...
4141
func NewProjectList(postOutputQueue *HttpPostOutputQueue, pathToInstallerParam string) *ProjectList {
4242

4343
result := &ProjectList{}
@@ -159,7 +159,7 @@ func (projectList *ProjectList) channelListener(postOutputQueue *HttpPostOutputQ
159159
}
160160
}
161161

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

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

178-
value.cliState.OnFileChangeEvent()
178+
if value.cliState != nil {
179+
value.cliState.OnFileChangeEvent(value.project.ProjectCreationTime)
180+
}
179181

180182
}
181183

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

312314
}
313315

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

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

323-
if currProjWatchState.project.PathToMonitor == projectToProcess.PathToMonitor {
324+
oldProjectToWatch := currProjWatchState.project
325+
326+
// This method may receive ProjectToWatch objects with either null or non-null
327+
// values for the `projectCreationTimeInAbsoluteMsecs` field. However, under no
328+
// circumstances should we ever replace a non-null value for this field with a
329+
// null field.
330+
//
331+
// For this reason, we carefully compare these values in this if block and
332+
// update accordingly.
333+
{
334+
pctUpdated := false
335+
336+
pctOldProjectToWatch := oldProjectToWatch.ProjectCreationTime
337+
pctNewProjectToWatch := projectToProcess.ProjectCreationTime
338+
339+
newPct := int64(0)
340+
341+
// If both the old and new values are not null, but the value has changed, then
342+
// use the new value.
343+
if pctNewProjectToWatch != 0 && pctOldProjectToWatch != 0 && pctNewProjectToWatch != pctOldProjectToWatch {
344+
345+
newPct = pctNewProjectToWatch
346+
347+
utils.LogInfo("The project creation time has changed, when both values were non-null. Old: " + timestampToString(pctOldProjectToWatch) + " New: " + timestampToString(pctNewProjectToWatch) + " for project " + projectToProcess.ProjectID)
348+
349+
pctUpdated = true
350+
}
351+
352+
// If old is not-null, and new is null, then DON'T overwrite the old one with
353+
// the new one.
354+
if pctOldProjectToWatch != 0 && pctNewProjectToWatch == 0 {
355+
356+
newPct = pctOldProjectToWatch
357+
358+
utils.LogInfo(
359+
"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)
360+
361+
newPtw := *(projectToProcess.Clone())
362+
newPtw.ProjectCreationTime = newPct
363+
364+
if newPtw.ProjectCreationTime != pctOldProjectToWatch {
365+
utils.LogSevere("Updated PTW field did not have correct projectCreationTime, for project " + projectToProcess.ProjectID)
366+
}
367+
368+
// Update the ptw, in case it is used by the following if block, but DONT call
369+
// po.updatePTW(...) with it.
370+
projectToProcess = newPtw
371+
pctUpdated = false // this is false so that updatePTW(...) is not called.
372+
}
373+
374+
// If the old is null, and the new is not null, then overwrite the old with the
375+
// new.
376+
if pctOldProjectToWatch == 0 && pctNewProjectToWatch != 0 {
377+
378+
newPct = pctNewProjectToWatch
379+
380+
utils.LogInfo("The project creation time has changed. Old: " + timestampToString(pctOldProjectToWatch) + " New: " + timestampToString(pctNewProjectToWatch) + ", for project " + projectToProcess.ProjectID)
381+
382+
pctUpdated = true
383+
384+
}
385+
386+
if pctUpdated {
324387

325-
oldProjectToWatch := currProjWatchState.project
388+
newPtw := *(projectToProcess.Clone())
389+
newPtw.ProjectCreationTime = newPct
390+
391+
// Update the object itself, in case the if-branch below this one is executed.
392+
projectToProcess = newPtw
393+
394+
// This logic may cause the PO to be updated twice (once here, and once below,
395+
// but this is fine)
396+
currProjWatchState.project = &projectToProcess
397+
}
398+
399+
}
400+
401+
if currProjWatchState.project.PathToMonitor == projectToProcess.PathToMonitor {
326402

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

384460
}
385461

462+
func timestampToString(ts int64) string {
463+
return strconv.FormatInt(ts, 10)
464+
}
465+
386466
/** This function is called with a new file change entry, which is filtered (if necessary) then patched to the project's batch utility object. */
387467
func handleReceiveNewWatchEventEntries(projectMatch *models.ProjectToWatch, entry *models.WatchEventEntry, projectsMap map[string]*projectObject) {
388468

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

480560
return &projectObject{
481561
&project,
482-
NewFileChangeEventBatchUtil(project.ProjectID, postOutputQueue, cliState),
562+
NewFileChangeEventBatchUtil(project.ProjectID, postOutputQueue, projectList),
483563
cliState, // May be null
484564
}, nil
485565
}

0 commit comments

Comments
 (0)