diff --git a/Filewatcherd-Go/src/codewind/clistate.go b/Filewatcherd-Go/src/codewind/clistate.go index 8f06db5..f3bd9f8 100644 --- a/Filewatcherd-Go/src/codewind/clistate.go +++ b/Filewatcherd-Go/src/codewind/clistate.go @@ -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 @@ -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." @@ -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 } @@ -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)) @@ -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 } @@ -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) { @@ -202,7 +207,7 @@ func (state *CLIState) runProjectCommand(timestamp int64) { spawnTimeInMsecs, } - state.channel <- CLIStateChannelEntry{&result} + state.channel <- CLIStateChannelEntry{0, &result} } else { @@ -215,7 +220,7 @@ func (state *CLIState) runProjectCommand(timestamp int64) { spawnTimeInMsecs, } - state.channel <- CLIStateChannelEntry{&result} + state.channel <- CLIStateChannelEntry{0, &result} } } diff --git a/Filewatcherd-Go/src/codewind/eventbatchutil.go b/Filewatcherd-Go/src/codewind/eventbatchutil.go index 571415b..2e2b06a 100644 --- a/Filewatcherd-Go/src/codewind/eventbatchutil.go +++ b/Filewatcherd-Go/src/codewind/eventbatchutil.go @@ -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) @@ -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 @@ -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 @@ -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 { diff --git a/Filewatcherd-Go/src/codewind/models/models.go b/Filewatcherd-Go/src/codewind/models/models.go index ca176fe..42276b0 100644 --- a/Filewatcherd-Go/src/codewind/models/models.go +++ b/Filewatcherd-Go/src/codewind/models/models.go @@ -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 @@ -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"` diff --git a/Filewatcherd-Go/src/codewind/projectlist.go b/Filewatcherd-Go/src/codewind/projectlist.go index c2e83a0..5bfeb91 100644 --- a/Filewatcherd-Go/src/codewind/projectlist.go +++ b/Filewatcherd-Go/src/codewind/projectlist.go @@ -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 @@ -38,6 +37,7 @@ type receiveNewWatchEntriesMessage struct { project *models.ProjectToWatch } +// NewProjectList ... func NewProjectList(postOutputQueue *HttpPostOutputQueue, pathToInstallerParam string) *ProjectList { result := &ProjectList{} @@ -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] @@ -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) + } } @@ -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 { @@ -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) { @@ -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 } diff --git a/Filewatcherd-TypeScript/src/lib/CLIState.ts b/Filewatcherd-TypeScript/src/lib/CLIState.ts index a7866ad..73668d1 100644 --- a/Filewatcherd-TypeScript/src/lib/CLIState.ts +++ b/Filewatcherd-TypeScript/src/lib/CLIState.ts @@ -47,7 +47,7 @@ export class CLIState { this._mockInstallerPath = process.env.MOCK_CWCTL_INSTALLER_PATH; } - public onFileChangeEvent() { + public onFileChangeEvent(projectCreationTimeInAbsoluteMsecsParam: number) { if (!this._projectPath || this._projectPath.trim().length === 0) { log.error("Project path passed to CLIState is empty, so ignoring file change event."); @@ -60,6 +60,28 @@ export class CLIState { } else { this._isProcessActive = true; this._isRequestWaiting = false; + + // Ensure that timestamp is updated with PCT, but only if timestamp is 0, + // AND there isn't a process running, AND pct is non-null. + { + const debugOldTimestampValue = this._timestamp; + + // We only update the timestamp when 'callCLI' is true, because we don't want to + // step on the toes of another running CLI process (and that one will probably + // update the timestamp on it's own, with a more recent value, then ours) + + // Update the timestamp to the project creation value, but ONLY IF it is zero. + if (projectCreationTimeInAbsoluteMsecsParam && projectCreationTimeInAbsoluteMsecsParam !== 0 + && this._timestamp === 0) { + + this._timestamp = projectCreationTimeInAbsoluteMsecsParam; + + log.info("Timestamp updated from " + debugOldTimestampValue + " to " + this._timestamp + + " from project creation time."); + + } + } + this.callCLIAsync(); // Do not await here } } @@ -106,7 +128,7 @@ export class CLIState { // If another file change list occurred during the last invocation, then start another one. if (this._isRequestWaiting) { - this.onFileChangeEvent(); + this.onFileChangeEvent(null); } } diff --git a/Filewatcherd-TypeScript/src/lib/FileWatcher.ts b/Filewatcherd-TypeScript/src/lib/FileWatcher.ts index f02f3c1..1b301c5 100644 --- a/Filewatcherd-TypeScript/src/lib/FileWatcher.ts +++ b/Filewatcherd-TypeScript/src/lib/FileWatcher.ts @@ -420,6 +420,7 @@ export class FileWatcher { const fileToMonitor = PathUtils.convertAbsoluteUnixStyleNormalizedPathToLocalFile(ptw.pathToMonitor); if (po === undefined) { + // If this is a new project to watch... let watchService = this._internalWatchService; @@ -445,8 +446,100 @@ export class FileWatcher { } else { + // Otherwise update existing project to watch, if needed const oldProjectToWatch = po.projectToWatch; + // 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 value. + // + // For this reason, we carefully compare these values in this if block and + // update accordingly. + { + let pctUpdated = false; + + const pctOldProjectToWatch = oldProjectToWatch.projectCreationTimeInAbsoluteMsecs; + const pctNewProjectToWatch = ptw.projectCreationTimeInAbsoluteMsecs; + + let newPct = null; + + // If both the old and new values are not null, but the value has changed, then + // use the new value. + if (pctNewProjectToWatch && pctOldProjectToWatch + && pctNewProjectToWatch !== pctOldProjectToWatch) { + + newPct = pctNewProjectToWatch; + + const newTimeInDate = pctNewProjectToWatch ? new Date(pctNewProjectToWatch).toString() + : ""; + + log.info("The project creation time has changed, when both values were non-null. Old: " + + pctOldProjectToWatch + " New: " + pctNewProjectToWatch + "(" + newTimeInDate + + "), for project " + ptw.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 && !pctNewProjectToWatch) { + + newPct = pctOldProjectToWatch; + + log.info( + "Internal project creation state was preserved, despite receiving a project " + + "update w/o this value. Current: " + pctOldProjectToWatch + " Received: " + + pctNewProjectToWatch + " for project " + ptw.projectId); + + // Update the ptw, in case it is used by the following if block, but DONT call + // po.updatePTW(...) with it. + if (ptw instanceof ProjectToWatchFromWebSocket) { + const castPtw = ptw as ProjectToWatchFromWebSocket; + ptw = ProjectToWatchFromWebSocket.cloneWebSocketWithNewProjectCreationTime(castPtw, newPct); + + } else if (ptw instanceof ProjectToWatch) { + const castPtw = ptw as ProjectToWatch; + ptw = ProjectToWatch.cloneWithNewProjectCreationTime(castPtw, newPct); + } + + // this is false so that updatePTW(...) is not called. + pctUpdated = false; + + } + + // If the old is null, and the new is not null, then overwrite the old with the + // new. + if (!pctOldProjectToWatch && pctNewProjectToWatch) { + newPct = pctNewProjectToWatch; + const newTimeInDate = newPct != null ? new Date(newPct).toString() : ""; + log.info("The project creation time has changed. Old: " + pctOldProjectToWatch + " New: " + + pctNewProjectToWatch + "(" + newTimeInDate + "), for project " + ptw.projectId); + + pctUpdated = true; + } + + if (pctUpdated) { + // Update the object itself, in case the if-branch below this one is executed. + + if (ptw instanceof ProjectToWatchFromWebSocket) { + const castPtw = ptw as ProjectToWatchFromWebSocket; + ptw = ProjectToWatchFromWebSocket.cloneWebSocketWithNewProjectCreationTime(castPtw, newPct); + + } else if (ptw instanceof ProjectToWatch) { + const castPtw = ptw as ProjectToWatch; + ptw = ProjectToWatch.cloneWithNewProjectCreationTime(castPtw, newPct); + } + + // This logic may cause the PO to be updated twice (once here, and once below, + // but this is fine) + po.updateProjectToWatch(ptw); + + } + + } + // If the watch has changed, then remove the path and update the PTw if (oldProjectToWatch.projectWatchStateId !== ptw.projectWatchStateId) { diff --git a/Filewatcherd-TypeScript/src/lib/HttpGetStatusThread.ts b/Filewatcherd-TypeScript/src/lib/HttpGetStatusThread.ts index 8bc70f3..4c6ed69 100644 --- a/Filewatcherd-TypeScript/src/lib/HttpGetStatusThread.ts +++ b/Filewatcherd-TypeScript/src/lib/HttpGetStatusThread.ts @@ -106,13 +106,13 @@ export class HttpGetStatusThread { for (const e of w.projects) { - // Santity check the json parsing + // Sanity check the JSON parsing if (!e.projectID || !e.pathToMonitor) { log.error("JSON parsing of GET watchlist endpoint failed with missing values"); return null; } - result.push(new ProjectToWatch(e, false)); + result.push(ProjectToWatch.createFromJson(e, false)); } return result; diff --git a/Filewatcherd-TypeScript/src/lib/Models.ts b/Filewatcherd-TypeScript/src/lib/Models.ts index 925f543..9c685cf 100644 --- a/Filewatcherd-TypeScript/src/lib/Models.ts +++ b/Filewatcherd-TypeScript/src/lib/Models.ts @@ -17,6 +17,7 @@ export interface IWatchedProjectJson { ignoredFilenames: string[]; changeType: string; type: string; + projectCreationTime: number; } export interface IWatchedProjectListJson { diff --git a/Filewatcherd-TypeScript/src/lib/ProjectObject.ts b/Filewatcherd-TypeScript/src/lib/ProjectObject.ts index d155615..bb3339a 100644 --- a/Filewatcherd-TypeScript/src/lib/ProjectObject.ts +++ b/Filewatcherd-TypeScript/src/lib/ProjectObject.ts @@ -63,7 +63,7 @@ export class ProjectObject { } public informCwctlOfFileChangesAsync() { - this._cliState.onFileChangeEvent(); + this._cliState.onFileChangeEvent(this._projectToWatch.projectCreationTimeInAbsoluteMsecs); } public get projectToWatch(): ProjectToWatch { diff --git a/Filewatcherd-TypeScript/src/lib/ProjectToWatch.ts b/Filewatcherd-TypeScript/src/lib/ProjectToWatch.ts index ca5eb4f..d1d8825 100644 --- a/Filewatcherd-TypeScript/src/lib/ProjectToWatch.ts +++ b/Filewatcherd-TypeScript/src/lib/ProjectToWatch.ts @@ -11,6 +11,7 @@ import * as models from "./Models"; import * as PathUtils from "./PathUtils"; +// import { ProjectToWatchFromWebSocket } from "./ProjectToWatchFromWebSocket"; /** * Contains information on what directory to (recursively monitor), and any @@ -23,54 +24,150 @@ import * as PathUtils from "./PathUtils"; */ export class ProjectToWatch { + public get projectId(): string { + return this._projectId; + } + + public get pathToMonitor(): string { + return this._pathToMonitor; + } + + public get ignoredPaths(): string[] { + return this._ignoredPaths; + } + + public get ignoredFilenames(): string[] { + return this._ignoredFilenames; + } + + public get projectWatchStateId(): string { + return this._projectWatchStateId; + } + + public get external(): boolean { + return this._external; + } + + public get projectCreationTimeInAbsoluteMsecs(): number { + return this._projectCreationTimeInAbsoluteMsecs; + } + + /** Create an instance of this class from the given JSON object. */ + public static createFromJson(json: models.IWatchedProjectJson, deleteChangeType: boolean): ProjectToWatch { + const result = new ProjectToWatch(); + ProjectToWatch.innerCreateFromJson(result, json, deleteChangeType); + return result; + } + + /** + * Create a new ProjectToWatch (not ProjectToWatchFromWebSocket), copy the values from the old param, + * but replace the projectCreationTimeInAbsoluteMsecs param + */ + public static cloneWithNewProjectCreationTime(old: ProjectToWatch, + projectCreationTimeInAbsoluteMsecsParam: number): ProjectToWatch { + + // if (old instanceof ProjectToWatchFromWebSocket) { + // // Sanity test + // throw new Error("cloneWithNewProjectCreationTime should not be called with a FromWebSocket object"); + // } + const result = new ProjectToWatch(); + + ProjectToWatch.copyWithNewProjectCreationTime(result, old, projectCreationTimeInAbsoluteMsecsParam); + + return result; + } + /** - * The contents of this class are immutable after creation, and should not be changed. - * Do not add non-readonly fields to this class. + * Copy values from old to result, but replace projectCreationTimeInAbsoluteMsecsParam param in result. + * Called only by above method, and from ProjectToWatchFromWebSocket. */ + protected static copyWithNewProjectCreationTime(result: ProjectToWatch, old: ProjectToWatch, + projectCreationTimeInAbsoluteMsecsParam: number) { - private readonly _projectId: string; - private readonly _pathToMonitor: string; + result._external = old.external; - private readonly _ignoredPaths: string[]; - private readonly _ignoredFilenames: string[]; + result._projectId = old.projectId; + result._pathToMonitor = old.pathToMonitor; - private readonly _projectWatchStateId: string; + result._projectWatchStateId = old.projectWatchStateId; - private readonly _external: boolean; + result.validatePathToMonitor(); - constructor(json: models.IWatchedProjectJson, deleteChangeType: boolean) { + const ignoredPaths: string[] = []; + if (old.ignoredPaths && old.ignoredPaths.length > 0) { + old.ignoredPaths.forEach((e) => { ignoredPaths.push(e); }); + } + result._ignoredPaths = ignoredPaths; + + const ignoredFilenames: string[] = []; + if (old.ignoredFilenames && old.ignoredFilenames.length > 0) { + old.ignoredFilenames.forEach((e) => { ignoredFilenames.push(e); }); + } + result._ignoredFilenames = ignoredFilenames; + + // Replace the old value, with specified parameter. + result._projectCreationTimeInAbsoluteMsecs = projectCreationTimeInAbsoluteMsecsParam; + + } + + /** Copy the values from the JSON object into the given ProjectToWatch. */ + protected static innerCreateFromJson(result: ProjectToWatch, json: models.IWatchedProjectJson, + deleteChangeType: boolean) { // Delete event from WebSocket only has these fields. if (deleteChangeType) { - this._projectId = json.projectID; - this._pathToMonitor = null; - this._projectWatchStateId = null; + result._projectId = json.projectID; + result._pathToMonitor = null; + result._projectWatchStateId = null; return; } - this._projectId = json.projectID; + result._projectId = json.projectID; - this._pathToMonitor = PathUtils.normalizeDriveLetter(json.pathToMonitor); + result._pathToMonitor = PathUtils.normalizeDriveLetter(json.pathToMonitor); - this.validatePathToMonitor(); + result.validatePathToMonitor(); const ignoredPaths: string[] = []; if (json.ignoredPaths && json.ignoredPaths.length > 0) { json.ignoredPaths.forEach((e) => { ignoredPaths.push(e); }); } - this._ignoredPaths = ignoredPaths; + result._ignoredPaths = ignoredPaths; const ignoredFilenames: string[] = []; if (json.ignoredFilenames && json.ignoredFilenames.length > 0) { json.ignoredFilenames.forEach((e) => { ignoredFilenames.push(e); }); } - this._ignoredFilenames = ignoredFilenames; + result._ignoredFilenames = ignoredFilenames; + + result._projectWatchStateId = json.projectWatchStateId; - this._projectWatchStateId = json.projectWatchStateId; + result._external = json.type ? (json.type.toLowerCase() === "non-project") : false; + + result._projectCreationTimeInAbsoluteMsecs = json.projectCreationTime; - this._external = json.type ? (json.type.toLowerCase() === "non-project") : false; } + /** + * The contents of this class are defacto immutable after creation, and should not be changed. + * However, the fields are not read-only because we need multiple constructing methods. + */ + + private _projectId: string; + private _pathToMonitor: string; + + private _ignoredPaths: string[]; + private _ignoredFilenames: string[]; + + private _projectWatchStateId: string; + + private _external: boolean; + + /** undefined if project time is not specified, a >0 value otherwise. */ + private _projectCreationTimeInAbsoluteMsecs: number; + + protected constructor() { } + private validatePathToMonitor() { if (this._pathToMonitor.indexOf("\\") !== -1) { @@ -88,28 +185,4 @@ export class ProjectToWatch { "Path to monitor may not end with path separator: " + this._pathToMonitor); } } - - public get projectId(): string { - return this._projectId; - } - - public get pathToMonitor(): string { - return this._pathToMonitor; - } - - public get ignoredPaths(): string[] { - return this._ignoredPaths; - } - - public get ignoredFilenames(): string[] { - return this._ignoredFilenames; - } - - public get projectWatchStateId(): string { - return this._projectWatchStateId; - } - - public get external(): boolean { - return this._external; - } } diff --git a/Filewatcherd-TypeScript/src/lib/ProjectToWatchFromWebSocket.ts b/Filewatcherd-TypeScript/src/lib/ProjectToWatchFromWebSocket.ts index c511da3..ad8068b 100644 --- a/Filewatcherd-TypeScript/src/lib/ProjectToWatchFromWebSocket.ts +++ b/Filewatcherd-TypeScript/src/lib/ProjectToWatchFromWebSocket.ts @@ -22,14 +22,37 @@ import { ProjectToWatch } from "./ProjectToWatch"; */ export class ProjectToWatchFromWebSocket extends ProjectToWatch { - private readonly _changeType: string; + public get changeType(): string { + return this._changeType; + } + + public static cloneWebSocketWithNewProjectCreationTime(old: ProjectToWatchFromWebSocket, + projectCreationTimeInAbsoluteMsecsParam: number) + : ProjectToWatchFromWebSocket { + + const result = new ProjectToWatchFromWebSocket(); + + ProjectToWatch.copyWithNewProjectCreationTime(result, old, projectCreationTimeInAbsoluteMsecsParam); + + result._changeType = old._changeType; - constructor(json: models.IWatchedProjectJson) { - super(json, json.changeType.toLowerCase() === "delete"); - this._changeType = json.changeType; + return result; } - public get changeType(): string { - return this._changeType; + public static create(json: models.IWatchedProjectJson): ProjectToWatchFromWebSocket { + const result = new ProjectToWatchFromWebSocket(); + ProjectToWatch.innerCreateFromJson(result, json, json.changeType.toLowerCase() === "delete"); + result._changeType = json.changeType; + + return result; + } + + private _changeType: string; + + protected constructor() { + // json: models.IWatchedProjectJson + super(); + // super(json, json.changeType.toLowerCase() === "delete"); + // this._changeType = json.changeType; } } diff --git a/Filewatcherd-TypeScript/src/lib/WebSocketManagerThread.ts b/Filewatcherd-TypeScript/src/lib/WebSocketManagerThread.ts index 73595b9..64f2fb2 100644 --- a/Filewatcherd-TypeScript/src/lib/WebSocketManagerThread.ts +++ b/Filewatcherd-TypeScript/src/lib/WebSocketManagerThread.ts @@ -228,6 +228,8 @@ export class WebSocketManagerThread { return; } + log.info("Received watch change message from WebSocket: " + s); + const wc: IWatchChangeJson = JSON.parse(s); if (!wc || !wc.type || !wc.projects) { @@ -241,7 +243,7 @@ export class WebSocketManagerThread { let infoStr = ""; for (const e of wc.projects) { - const ptw: ProjectToWatchFromWebSocket = new ProjectToWatchFromWebSocket(e); + const ptw: ProjectToWatchFromWebSocket = ProjectToWatchFromWebSocket.create(e); projects.push(ptw); infoStr += "[" + ptw.projectId + " in " + (ptw.pathToMonitor ? ptw.pathToMonitor : "N/A") + "], "; }