/// <summary> /// Run the specified script. /// </summary> /// <param name="script">Script.</param> /// <param name="previousDatum">Previous datum.</param> /// <param name="currentDatum">Current datum.</param> private void Run(Script script, Datum previousDatum = null, Datum currentDatum = null) { SensusServiceHelper.Get().Logger.Log($"Running \"{Name}\".", LoggingLevel.Normal, GetType()); script.RunTime = DateTimeOffset.UtcNow; // scheduled scripts have their expiration dates set when they're scheduled. scripts triggered by other probes // as well as on-start scripts will not yet have their expiration dates set. so check the script we've been // given and set the expiration date if needed. triggered scripts don't have windows, so the only expiration // condition comes from the maximum age. if (script.ExpirationDate == null && _maxAge.HasValue) { script.ExpirationDate = script.Birthdate + _maxAge.Value; } // script could have already expired (e.g., if user took too long to open notification). if (script.ExpirationDate.HasValue && script.ExpirationDate.Value < DateTime.Now) { SensusServiceHelper.Get().Logger.Log("Script expired before it was run.", LoggingLevel.Normal, GetType()); return; } // do not run a one-shot script if it has already been run if (OneShot && RunTimes.Count > 0) { SensusServiceHelper.Get().Logger.Log("Not running one-shot script multiple times.", LoggingLevel.Normal, GetType()); return; } lock (RunTimes) { // track participation by recording the current time. use this instead of the script's run timestamp, since // the latter is the time of notification on ios rather than the time that the user actually viewed the script. RunTimes.Add(DateTime.Now); RunTimes.RemoveAll(r => r < Probe.Protocol.ParticipationHorizon); } #region submit a separate datum indicating each time the script was run. Task.Run(async() => { // geotag the script-run datum if any of the input groups are also geotagged. if none of the groups are geotagged, then // it wouldn't make sense to gather location data from a user. double?latitude = null; double?longitude = null; DateTimeOffset?locationTimestamp = null; if (script.InputGroups.Any(inputGroup => inputGroup.Geotag)) { try { Position currentPosition = GpsReceiver.Get().GetReading(new CancellationToken(), false); if (currentPosition == null) { throw new Exception("GPS receiver returned null position."); } latitude = currentPosition.Latitude; longitude = currentPosition.Longitude; locationTimestamp = currentPosition.Timestamp; } catch (Exception ex) { SensusServiceHelper.Get().Logger.Log("Failed to get position for script-run datum: " + ex.Message, LoggingLevel.Normal, GetType()); } } await Probe.StoreDatumAsync(new ScriptRunDatum(script.RunTime.Value, Script.Id, Name, script.Id, script.ScheduledRunTime, script.CurrentDatum?.Id, latitude, longitude, locationTimestamp), default(CancellationToken)); }); #endregion // this method can be called with previous / current datum values (e.g., when the script is first triggered). it // can also be called without previous / current datum values (e.g., when triggering on a schedule). if // we have such values, set them on the script. if (previousDatum != null) { script.PreviousDatum = previousDatum; } if (currentDatum != null) { script.CurrentDatum = currentDatum; } SensusServiceHelper.Get().AddScriptToRun(script, RunMode); }
public async Task RunAsync(Script script, Datum previousDatum = null, Datum currentDatum = null) { SensusServiceHelper.Get().Logger.Log($"Running \"{Name}\".", LoggingLevel.Normal, GetType()); script.RunTime = DateTimeOffset.UtcNow; // this method can be called with previous / current datum values (e.g., when the script is first triggered). it // can also be called without previous / current datum values (e.g., when triggering on a schedule). if // we have such values, set them on the script. if (previousDatum != null) { script.PreviousDatum = previousDatum; } if (currentDatum != null) { script.CurrentDatum = currentDatum; } // scheduled scripts have their expiration dates set when they're scheduled. scripts triggered by other probes // as well as on-start scripts will not yet have their expiration dates set. so check the script we've been // given and set the expiration date if needed. triggered scripts don't have windows, so the only expiration // condition comes from the maximum age. if (script.ExpirationDate == null && _maxAge.HasValue) { script.ExpirationDate = script.Birthdate + _maxAge.Value; } // script could have already expired (e.g., if user took too long to open notification). if (script.ExpirationDate.HasValue && script.ExpirationDate.Value < DateTime.Now) { SensusServiceHelper.Get().Logger.Log("Script expired before it was run.", LoggingLevel.Normal, GetType()); return; } // do not run a one-shot script if it has already been run if (OneShot && RunTimes.Count > 0) { SensusServiceHelper.Get().Logger.Log("Not running one-shot script multiple times.", LoggingLevel.Normal, GetType()); return; } // check with the survey agent if there is one if (Probe.Agent != null) { Tuple <bool, DateTimeOffset?> deliverFutureTime = await Probe.Agent.DeliverSurveyNowAsync(script); if (deliverFutureTime.Item1) { Probe.Protocol.LocalDataStore.WriteDatum(new ScriptStateDatum(ScriptState.AgentAccepted, script.RunTime.Value, script), CancellationToken.None); } else { if (deliverFutureTime.Item2 == null) { SensusServiceHelper.Get().Logger.Log("Agent has declined survey without deferral.", LoggingLevel.Normal, GetType()); Probe.Protocol.LocalDataStore.WriteDatum(new ScriptStateDatum(ScriptState.AgentDeclined, script.RunTime.Value, script), CancellationToken.None); } else if (deliverFutureTime.Item2.Value > DateTimeOffset.UtcNow) { SensusServiceHelper.Get().Logger.Log("Agent has deferred survey until: " + deliverFutureTime.Item2.Value, LoggingLevel.Normal, GetType()); Probe.Protocol.LocalDataStore.WriteDatum(new ScriptStateDatum(ScriptState.AgentDeferred, script.RunTime.Value, script), CancellationToken.None); // check whether we need to expire the rescheduled script at some future point DateTime?expiration = null; DateTime trigger = deliverFutureTime.Item2.Value.LocalDateTime; if (_maxAge.HasValue) { expiration = trigger + _maxAge.Value; } // there is no window, so just add a descriptive, unique descriptor in place of the window ScriptTriggerTime triggerTime = new ScriptTriggerTime(trigger, expiration, "DEFERRED-" + Guid.NewGuid()); // schedule the trigger. since this is a deferral, use the same script identifier that we currently have. this // will maintain consistency and interpretability of the ScriptStateDatum objects that are recording the progression // of scripts. this will also let survey agents better interpret what's going on with deferrals. this identifier is // used as the RunId in the various tracked data types. await ScheduleScriptRunAsync(triggerTime, script.Id); } else { SensusServiceHelper.Get().Logger.Log("Warning: Agent has deferred survey to a time in the past: " + deliverFutureTime.Item2.Value, LoggingLevel.Normal, GetType()); } // do not proceed. the calling method (if scheduler-based) will take care of removing the current script. return; } } lock (RunTimes) { // track participation by recording the current time. use this instead of the script's run timestamp, since // the latter is the time of notification on ios rather than the time that the user actually viewed the script. RunTimes.Add(DateTime.Now); RunTimes.RemoveAll(r => r < Probe.Protocol.ParticipationHorizon); } await SensusServiceHelper.Get().AddScriptAsync(script, RunMode); // let the script agent know and store a datum to record the event await(Probe.Agent?.ObserveAsync(script, ScriptState.Delivered) ?? Task.CompletedTask); Probe.Protocol.LocalDataStore.WriteDatum(new ScriptStateDatum(ScriptState.Delivered, script.RunTime.Value, script), CancellationToken.None); }