You've already forked linux-packaging-mono
Imported Upstream version 4.6.0.125
Former-commit-id: a2155e9bd80020e49e72e86c44da02a8ac0e57a4
This commit is contained in:
parent
a569aebcfd
commit
e79aa3c0ed
@@ -0,0 +1,80 @@
|
||||
// <copyright>
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace System.Activities.Debugger
|
||||
{
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
[DebuggerDisplay("{this.ToString()}")]
|
||||
internal class BinarySearchResult
|
||||
{
|
||||
private int result;
|
||||
private int count;
|
||||
|
||||
internal BinarySearchResult(int resultFromBinarySearch, int count)
|
||||
{
|
||||
this.result = resultFromBinarySearch;
|
||||
this.count = count;
|
||||
}
|
||||
|
||||
internal bool IsFound
|
||||
{
|
||||
get { return this.result >= 0; }
|
||||
}
|
||||
|
||||
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
|
||||
internal int FoundIndex
|
||||
{
|
||||
get
|
||||
{
|
||||
UnitTestUtility.Assert(this.IsFound, "We should not call FoundIndex if we cannot find the element.");
|
||||
return this.result;
|
||||
}
|
||||
}
|
||||
|
||||
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
|
||||
internal int NextIndex
|
||||
{
|
||||
get
|
||||
{
|
||||
UnitTestUtility.Assert(!this.IsFound, "We should not call NextIndex if we found the element.");
|
||||
UnitTestUtility.Assert(this.IsNextIndexAvailable, "We should not call NextIndex if next index is not available.");
|
||||
return this.NextIndexValue;
|
||||
}
|
||||
}
|
||||
|
||||
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
|
||||
internal bool IsNextIndexAvailable
|
||||
{
|
||||
get
|
||||
{
|
||||
UnitTestUtility.Assert(!this.IsFound, "We should not call IsNextIndexAvailable if we found the element.");
|
||||
return this.NextIndexValue != this.count;
|
||||
}
|
||||
}
|
||||
|
||||
private int NextIndexValue
|
||||
{
|
||||
get { return ~this.result; }
|
||||
}
|
||||
|
||||
[SuppressMessage("Microsoft.Globalization", "CA1305:SpecifyIFormatProvider", MessageId = "System.String.Format(System.String,System.Object)", Justification = "Message used in debugger only.")]
|
||||
public override string ToString()
|
||||
{
|
||||
if (this.IsFound)
|
||||
{
|
||||
return string.Format("Data is found at index {0}.", this.FoundIndex);
|
||||
}
|
||||
else if (this.IsNextIndexAvailable)
|
||||
{
|
||||
return string.Format("Data is not found, the next index is {0}.", this.NextIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
return "Data is not found and there is no next index.";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,41 @@
|
||||
// <copyright>
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace System.Activities.Debugger
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
|
||||
internal partial class CharacterSpottingTextReader : ICharacterSpottingTextReaderForUnitTest
|
||||
{
|
||||
int ICharacterSpottingTextReaderForUnitTest.CurrentLine
|
||||
{
|
||||
get { return this.currentLine; }
|
||||
}
|
||||
|
||||
int ICharacterSpottingTextReaderForUnitTest.CurrentPosition
|
||||
{
|
||||
get { return this.currentPosition; }
|
||||
}
|
||||
|
||||
List<DocumentLocation> ICharacterSpottingTextReaderForUnitTest.StartBrackets
|
||||
{
|
||||
get { return this.startAngleBrackets; }
|
||||
}
|
||||
|
||||
List<DocumentLocation> ICharacterSpottingTextReaderForUnitTest.EndBrackets
|
||||
{
|
||||
get { return this.endAngleBrackets; }
|
||||
}
|
||||
|
||||
List<DocumentLocation> ICharacterSpottingTextReaderForUnitTest.SingleQuotes
|
||||
{
|
||||
get { return this.singleQuotes; }
|
||||
}
|
||||
|
||||
List<DocumentLocation> ICharacterSpottingTextReaderForUnitTest.DoubleQuotes
|
||||
{
|
||||
get { return this.doubleQuotes; }
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,214 @@
|
||||
// <copyright>
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace System.Activities.Debugger
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
//
|
||||
internal partial class CharacterSpottingTextReader : TextReader
|
||||
{
|
||||
// These 'special characters' couple with the fact that we are working on XML.
|
||||
private const char StartAngleBracket = '<';
|
||||
private const char EndAngleBracket = '>';
|
||||
private const char SingleQuote = '\'';
|
||||
private const char DoubleQuote = '"';
|
||||
private const char EndLine = '\n';
|
||||
private const char CarriageReturn = '\r';
|
||||
|
||||
private TextReader underlyingReader;
|
||||
private int currentLine;
|
||||
private int currentPosition;
|
||||
private List<DocumentLocation> startAngleBrackets;
|
||||
private List<DocumentLocation> endAngleBrackets;
|
||||
private List<DocumentLocation> singleQuotes;
|
||||
private List<DocumentLocation> doubleQuotes;
|
||||
private List<DocumentLocation> endLines;
|
||||
|
||||
public CharacterSpottingTextReader(TextReader underlyingReader)
|
||||
{
|
||||
UnitTestUtility.Assert(underlyingReader != null, "underlyingReader should not be null and should be ensured by caller.");
|
||||
this.underlyingReader = underlyingReader;
|
||||
this.currentLine = 1;
|
||||
this.currentPosition = 1;
|
||||
this.startAngleBrackets = new List<DocumentLocation>();
|
||||
this.endAngleBrackets = new List<DocumentLocation>();
|
||||
this.singleQuotes = new List<DocumentLocation>();
|
||||
this.doubleQuotes = new List<DocumentLocation>();
|
||||
this.endLines = new List<DocumentLocation>();
|
||||
}
|
||||
|
||||
// CurrentLocation consists of the current line number and the current position on the line.
|
||||
//
|
||||
// The current position is like a cursor moving along the line. For example, a string "abc" ending with "\r\n":
|
||||
//
|
||||
// abc\r\n
|
||||
//
|
||||
// the current position, depicted as | below, moves from char to char:
|
||||
//
|
||||
// |a|b|c|\r|\n
|
||||
//
|
||||
// When we are at the beginning of the line, the current position is 1. After we read the first char,
|
||||
// we advance the current position to 2, and so on:
|
||||
//
|
||||
// 1 2 3 4
|
||||
// |a|b|c|\r|\n
|
||||
//
|
||||
// As we reach the end-of-line character on the line, which can be \r, \r\n or \n, we move to the next line and reset the current position to 1.
|
||||
private DocumentLocation CurrentLocation
|
||||
{
|
||||
get
|
||||
{
|
||||
return new DocumentLocation(this.currentLine, this.currentPosition);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Close()
|
||||
{
|
||||
this.underlyingReader.Close();
|
||||
}
|
||||
|
||||
public override int Peek()
|
||||
{
|
||||
// This character is not consider read, therefore we don't need to analyze this.
|
||||
return this.underlyingReader.Peek();
|
||||
}
|
||||
|
||||
public override int Read()
|
||||
{
|
||||
int result = this.underlyingReader.Read();
|
||||
if (result != -1)
|
||||
{
|
||||
result = this.AnalyzeReadData((char)result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
internal DocumentLocation FindCharacterStrictlyAfter(char c, DocumentLocation afterLocation)
|
||||
{
|
||||
List<DocumentLocation> locationList = this.GetLocationList(c);
|
||||
UnitTestUtility.Assert(locationList != null, "We should always find character for special characters only");
|
||||
|
||||
// Note that this 'nextLocation' may not represent a real document location (we could hit an end line character here so that there is no next line
|
||||
// position. This is merely used for the search algorithm below:
|
||||
DocumentLocation nextLocation = new DocumentLocation(afterLocation.LineNumber, new OneBasedCounter(afterLocation.LinePosition.Value + 1));
|
||||
BinarySearchResult result = locationList.MyBinarySearch(nextLocation);
|
||||
if (result.IsFound)
|
||||
{
|
||||
// It is possible that the next location is a quote itself, or
|
||||
return nextLocation;
|
||||
}
|
||||
else if (result.IsNextIndexAvailable)
|
||||
{
|
||||
// Some other later position is the quote, or
|
||||
return locationList[result.NextIndex];
|
||||
}
|
||||
else
|
||||
{
|
||||
// in the worst case no quote can be found.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
internal DocumentLocation FindCharacterStrictlyBefore(char c, DocumentLocation documentLocation)
|
||||
{
|
||||
List<DocumentLocation> locationList = this.GetLocationList(c);
|
||||
UnitTestUtility.Assert(locationList != null, "We should always find character for special characters only");
|
||||
|
||||
BinarySearchResult result = locationList.MyBinarySearch(documentLocation);
|
||||
if (result.IsFound)
|
||||
{
|
||||
if (result.FoundIndex > 0)
|
||||
{
|
||||
return locationList[result.FoundIndex - 1];
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
else if (result.IsNextIndexAvailable)
|
||||
{
|
||||
if (result.NextIndex > 0)
|
||||
{
|
||||
return locationList[result.NextIndex - 1];
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
else if (locationList.Count > 0)
|
||||
{
|
||||
return locationList[locationList.Count - 1];
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private List<DocumentLocation> GetLocationList(char c)
|
||||
{
|
||||
switch (c)
|
||||
{
|
||||
case StartAngleBracket:
|
||||
return this.startAngleBrackets;
|
||||
case EndAngleBracket:
|
||||
return this.endAngleBrackets;
|
||||
case SingleQuote:
|
||||
return this.singleQuotes;
|
||||
case DoubleQuote:
|
||||
return this.doubleQuotes;
|
||||
case EndLine:
|
||||
return this.endLines;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Process last character read, and canonicalize end line.
|
||||
/// </summary>
|
||||
/// <param name="lastCharacterRead">The last character read by the underlying reader</param>
|
||||
/// <returns>The last character processed</returns>
|
||||
private char AnalyzeReadData(char lastCharacterRead)
|
||||
{
|
||||
// XML specification requires end-of-line == '\n' or '\r' or "\r\n"
|
||||
// See http://www.w3.org/TR/2008/REC-xml-20081126/#sec-line-ends for details.
|
||||
if (lastCharacterRead == CarriageReturn)
|
||||
{
|
||||
// if reading \r and peek next char is \n, then process \n as well
|
||||
int nextChar = this.underlyingReader.Peek();
|
||||
if (nextChar == EndLine)
|
||||
{
|
||||
lastCharacterRead = (char)this.underlyingReader.Read();
|
||||
}
|
||||
}
|
||||
|
||||
if (lastCharacterRead == EndLine || lastCharacterRead == CarriageReturn)
|
||||
{
|
||||
this.endLines.Add(this.CurrentLocation);
|
||||
this.currentLine++;
|
||||
this.currentPosition = 1;
|
||||
|
||||
// according to XML spec, both \r\n and \r should be translated to \n
|
||||
return EndLine;
|
||||
}
|
||||
else
|
||||
{
|
||||
List<DocumentLocation> locations = this.GetLocationList(lastCharacterRead);
|
||||
if (locations != null)
|
||||
{
|
||||
locations.Add(this.CurrentLocation);
|
||||
}
|
||||
|
||||
this.currentPosition++;
|
||||
return lastCharacterRead;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,100 @@
|
||||
//----------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
//----------------------------------------------------------------
|
||||
|
||||
namespace System.Activities.Debugger
|
||||
{
|
||||
using System;
|
||||
using System.Activities.Hosting;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq.Expressions;
|
||||
using System.Runtime;
|
||||
using System.Activities.Validation;
|
||||
|
||||
// DebugController, one is needed per ActivityExecutor.
|
||||
[DebuggerNonUserCode]
|
||||
class DebugController
|
||||
{
|
||||
WorkflowInstance host;
|
||||
DebugManager debugManager; // Instantiated after first instrumentation is successful.
|
||||
|
||||
public DebugController(WorkflowInstance host)
|
||||
{
|
||||
this.host = host;
|
||||
}
|
||||
|
||||
public void WorkflowStarted()
|
||||
{
|
||||
}
|
||||
|
||||
public void WorkflowCompleted()
|
||||
{
|
||||
if (this.debugManager != null)
|
||||
{
|
||||
this.debugManager.Exit();
|
||||
this.debugManager = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void ActivityStarted(ActivityInstance activityInstance)
|
||||
{
|
||||
if (!(activityInstance.Activity.RootActivity is Constraint)) // Don't debug an activity in a Constraint
|
||||
{
|
||||
EnsureActivityInstrumented(activityInstance, false);
|
||||
this.debugManager.OnEnterState(activityInstance);
|
||||
}
|
||||
}
|
||||
|
||||
public void ActivityCompleted(ActivityInstance activityInstance)
|
||||
{
|
||||
if (!(activityInstance.Activity.RootActivity is Constraint)) // Don't debug an activity in a Constraint
|
||||
{
|
||||
EnsureActivityInstrumented(activityInstance, true);
|
||||
this.debugManager.OnLeaveState(activityInstance);
|
||||
}
|
||||
}
|
||||
|
||||
// Lazy instrumentation.
|
||||
// Parameter primeCurrentInstance specify whether priming (if needed) is done
|
||||
// up to the current instance. Set this to true when calling this from an "...Completed"
|
||||
// (exit state).
|
||||
void EnsureActivityInstrumented(ActivityInstance instance, bool primeCurrentInstance)
|
||||
{
|
||||
if (this.debugManager == null)
|
||||
{ // Workflow has not been instrumented yet.
|
||||
|
||||
// Finding rootInstance and check all referred sources.
|
||||
Stack<ActivityInstance> ancestors = new Stack<ActivityInstance>();
|
||||
while (instance.Parent != null)
|
||||
{
|
||||
ancestors.Push(instance);
|
||||
instance = instance.Parent;
|
||||
}
|
||||
|
||||
Activity rootActivity = instance.Activity;
|
||||
|
||||
// Do breakOnStartup only if debugger is attached from the beginning, i.e. no priming needed.
|
||||
// This specified by change the last parameter below to: "(ancestors.Count == 0)".
|
||||
this.debugManager = new DebugManager(rootActivity, "Workflow", "Workflow", "DebuggerThread", false, this.host, ancestors.Count == 0);
|
||||
|
||||
if (ancestors.Count > 0)
|
||||
{
|
||||
// Priming the background thread
|
||||
this.debugManager.IsPriming = true;
|
||||
while (ancestors.Count > 0)
|
||||
{
|
||||
ActivityInstance ancestorInstance = ancestors.Pop();
|
||||
this.debugManager.OnEnterState(ancestorInstance);
|
||||
}
|
||||
if (primeCurrentInstance)
|
||||
{
|
||||
this.debugManager.OnEnterState(instance);
|
||||
}
|
||||
this.debugManager.IsPriming = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,74 @@
|
||||
// <copyright>
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace System.Activities.Debugger
|
||||
{
|
||||
using System.Diagnostics;
|
||||
|
||||
// Immutable, lineNumber and linePosition always non-null.
|
||||
[DebuggerDisplay("({LineNumber.Value}:{LinePosition.Value})")]
|
||||
internal class DocumentLocation : IEquatable<DocumentLocation>, IComparable<DocumentLocation>
|
||||
{
|
||||
private OneBasedCounter lineNumber;
|
||||
private OneBasedCounter linePosition;
|
||||
|
||||
internal DocumentLocation(OneBasedCounter lineNumber, OneBasedCounter linePosition)
|
||||
{
|
||||
UnitTestUtility.Assert(lineNumber != null, "lineNumber should not be null.");
|
||||
UnitTestUtility.Assert(linePosition != null, "linePosition should not be null.");
|
||||
this.lineNumber = lineNumber;
|
||||
this.linePosition = linePosition;
|
||||
}
|
||||
|
||||
internal DocumentLocation(int lineNumber, int linePosition)
|
||||
: this(new OneBasedCounter(lineNumber), new OneBasedCounter(linePosition))
|
||||
{
|
||||
}
|
||||
|
||||
internal OneBasedCounter LineNumber
|
||||
{
|
||||
get { return this.lineNumber; }
|
||||
}
|
||||
|
||||
internal OneBasedCounter LinePosition
|
||||
{
|
||||
get { return this.linePosition; }
|
||||
}
|
||||
|
||||
public bool Equals(DocumentLocation that)
|
||||
{
|
||||
if (that == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return (this.lineNumber.Value == that.lineNumber.Value) && (this.linePosition.Value == that.linePosition.Value);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return this.lineNumber.Value.GetHashCode() ^ this.linePosition.Value.GetHashCode();
|
||||
}
|
||||
|
||||
public int CompareTo(DocumentLocation that)
|
||||
{
|
||||
if (that == null)
|
||||
{
|
||||
// Following the convention we have in System.Int32 that anything is considered bigger than null.
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (this.lineNumber.Value == that.lineNumber.Value)
|
||||
{
|
||||
// The subtraction of two numbers >= 1 must not underflow integer.
|
||||
return this.linePosition.Value - that.linePosition.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
// The subtraction of two numbers >= 1 must not underflow integer.
|
||||
return this.lineNumber.Value - that.lineNumber.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,55 @@
|
||||
// <copyright>
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace System.Activities.Debugger
|
||||
{
|
||||
using System.Diagnostics;
|
||||
|
||||
// Immutable, start and end always non-null.
|
||||
[DebuggerDisplay("({Start.LineNumber.Value}:{Start.LinePosition.Value}) - ({End.LineNumber.Value}:{End.LinePosition.Value})")]
|
||||
internal class DocumentRange : IEquatable<DocumentRange>
|
||||
{
|
||||
private DocumentLocation start;
|
||||
private DocumentLocation end;
|
||||
|
||||
internal DocumentRange(DocumentLocation start, DocumentLocation end)
|
||||
{
|
||||
UnitTestUtility.Assert(start != null, "DocumentRange.Start cannot be null");
|
||||
UnitTestUtility.Assert(end != null, "DocumentRange.End cannot be null");
|
||||
UnitTestUtility.Assert((start.LineNumber.Value < end.LineNumber.Value) || ((start.LineNumber.Value == end.LineNumber.Value) && (start.LinePosition.Value <= end.LinePosition.Value)), "Start cannot before go after End.");
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
}
|
||||
|
||||
internal DocumentRange(int startLineNumber, int startLinePosition, int endLineNumber, int endLinePosition)
|
||||
: this(new DocumentLocation(startLineNumber, startLinePosition), new DocumentLocation(endLineNumber, endLinePosition))
|
||||
{
|
||||
}
|
||||
|
||||
internal DocumentLocation Start
|
||||
{
|
||||
get { return this.start; }
|
||||
}
|
||||
|
||||
internal DocumentLocation End
|
||||
{
|
||||
get { return this.end; }
|
||||
}
|
||||
|
||||
public bool Equals(DocumentRange other)
|
||||
{
|
||||
if (other == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.Start.Equals(other.Start) && this.End.Equals(other.End);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return this.Start.GetHashCode() ^ this.End.GetHashCode();
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,23 @@
|
||||
// <copyright>
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace System.Activities.Debugger
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
|
||||
internal interface ICharacterSpottingTextReaderForUnitTest
|
||||
{
|
||||
int CurrentLine { get; }
|
||||
|
||||
int CurrentPosition { get; }
|
||||
|
||||
List<DocumentLocation> StartBrackets { get; }
|
||||
|
||||
List<DocumentLocation> EndBrackets { get; }
|
||||
|
||||
List<DocumentLocation> SingleQuotes { get; }
|
||||
|
||||
List<DocumentLocation> DoubleQuotes { get; }
|
||||
}
|
||||
}
|
@@ -0,0 +1,15 @@
|
||||
//-----------------------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
namespace System.Activities.Debugger
|
||||
{
|
||||
// Interface to implement in serializable object containing Workflow
|
||||
// to be debuggable with Workflow debugger.
|
||||
public interface IDebuggableWorkflowTree
|
||||
{
|
||||
// Return the root of the workflow tree.
|
||||
Activity GetWorkflowRoot();
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,116 @@
|
||||
//----------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
//----------------------------------------------------------------
|
||||
|
||||
namespace System.Activities.Debugger
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
using System.Activities.Debugger.Symbol;
|
||||
|
||||
// Keep track of instrumentation information.
|
||||
// - which subroot has source file but not yet instrumented.
|
||||
// - which subroots share the same source file
|
||||
// SubRoot is defined as an activity that has a source file
|
||||
// (Custom Activity).
|
||||
class InstrumentationTracker
|
||||
{
|
||||
// Root of the workflow to keep track.
|
||||
Activity root;
|
||||
|
||||
// Mapping of subroots to their source files.
|
||||
Dictionary<Activity, string> uninstrumentedSubRoots;
|
||||
|
||||
Dictionary<Activity, string> UninstrumentedSubRoots
|
||||
{
|
||||
get
|
||||
{
|
||||
if (this.uninstrumentedSubRoots == null)
|
||||
{
|
||||
InitializeUninstrumentedSubRoots();
|
||||
}
|
||||
return this.uninstrumentedSubRoots;
|
||||
}
|
||||
}
|
||||
|
||||
public InstrumentationTracker(Activity root)
|
||||
{
|
||||
this.root = root;
|
||||
}
|
||||
|
||||
// Initialize UninstrumentedSubRoots by traversing the workflow.
|
||||
void InitializeUninstrumentedSubRoots()
|
||||
{
|
||||
this.uninstrumentedSubRoots = new Dictionary<Activity, string>();
|
||||
|
||||
Queue<Activity> activitiesRemaining = new Queue<Activity>();
|
||||
|
||||
CollectSubRoot(this.root);
|
||||
activitiesRemaining.Enqueue(this.root);
|
||||
|
||||
while (activitiesRemaining.Count > 0)
|
||||
{
|
||||
Activity toProcess = activitiesRemaining.Dequeue();
|
||||
|
||||
foreach (Activity activity in WorkflowInspectionServices.GetActivities(toProcess))
|
||||
{
|
||||
if (!uninstrumentedSubRoots.ContainsKey(activity))
|
||||
{
|
||||
CollectSubRoot(activity);
|
||||
activitiesRemaining.Enqueue(activity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Collect subroot as uninstrumented activity.
|
||||
void CollectSubRoot(Activity activity)
|
||||
{
|
||||
string wfSymbol = DebugSymbol.GetSymbol(activity) as string;
|
||||
if (!string.IsNullOrEmpty(wfSymbol))
|
||||
{
|
||||
this.uninstrumentedSubRoots.Add(activity, wfSymbol);
|
||||
}
|
||||
else
|
||||
{
|
||||
string sourcePath = XamlDebuggerXmlReader.GetFileName(activity) as string;
|
||||
if (!string.IsNullOrEmpty(sourcePath))
|
||||
{
|
||||
this.uninstrumentedSubRoots.Add(activity, sourcePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Whether this is unistrumented sub root.
|
||||
public bool IsUninstrumentedSubRoot(Activity subRoot)
|
||||
{
|
||||
return this.UninstrumentedSubRoots.ContainsKey(subRoot);
|
||||
}
|
||||
|
||||
|
||||
// Returns Activities that have the same source as the given subRoot.
|
||||
// This will return other instantiation of the same custom activity.
|
||||
// Needed to avoid re-instrumentation of the same file.
|
||||
public List<Activity> GetSameSourceSubRoots(Activity subRoot)
|
||||
{
|
||||
string sourcePath;
|
||||
List<Activity> sameSourceSubRoots = new List<Activity>();
|
||||
if (this.UninstrumentedSubRoots.TryGetValue(subRoot, out sourcePath))
|
||||
{
|
||||
foreach (KeyValuePair<Activity, string> entry in this.UninstrumentedSubRoots)
|
||||
{
|
||||
if (entry.Value == sourcePath && entry.Key != subRoot)
|
||||
{
|
||||
sameSourceSubRoots.Add(entry.Key);
|
||||
}
|
||||
}
|
||||
}
|
||||
return sameSourceSubRoots;
|
||||
}
|
||||
|
||||
// Mark this sub root as instrumented.
|
||||
public void MarkInstrumented(Activity subRoot)
|
||||
{
|
||||
this.UninstrumentedSubRoots.Remove(subRoot);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,16 @@
|
||||
// <copyright>
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace System.Activities.Debugger
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
|
||||
internal static class ListExtensions
|
||||
{
|
||||
internal static BinarySearchResult MyBinarySearch<T>(this List<T> input, T item)
|
||||
{
|
||||
return new BinarySearchResult(input.BinarySearch(item), input.Count);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,44 @@
|
||||
//-----------------------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
namespace System.Activities.Debugger
|
||||
{
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime;
|
||||
|
||||
// Class to describe the name and type for an early bound local
|
||||
// that will show up in Locals window.
|
||||
[DebuggerNonUserCode]
|
||||
[Fx.Tag.XamlVisible(false)]
|
||||
public class LocalsItemDescription
|
||||
{
|
||||
public LocalsItemDescription(string name, Type type)
|
||||
{
|
||||
this.Name = name;
|
||||
this.Type = type;
|
||||
}
|
||||
|
||||
public string Name
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
[SuppressMessage(FxCop.Category.Naming, FxCop.Rule.PropertyNamesShouldNotMatchGetMethods,
|
||||
Justification = "Workflow normalizes on Type for Type properties")]
|
||||
public Type Type
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return this.Name + ":" + this.Type.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,23 @@
|
||||
// <copyright>
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace System.Activities.Debugger
|
||||
{
|
||||
// Immutable, value >= 1
|
||||
internal class OneBasedCounter
|
||||
{
|
||||
private int value;
|
||||
|
||||
internal OneBasedCounter(int value)
|
||||
{
|
||||
UnitTestUtility.Assert(value > 0, "value cannot less than one for OneBasedCounter");
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
internal int Value
|
||||
{
|
||||
get { return this.value; }
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,198 @@
|
||||
//-----------------------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
namespace System.Activities.Debugger
|
||||
{
|
||||
using System;
|
||||
using System.Activities.Debugger.Symbol;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Runtime;
|
||||
|
||||
// Identifies a specific location in the target source code.
|
||||
//
|
||||
// This source information is used in creating PDBs, which will be passed to the debugger,
|
||||
// which will resolve the source file based off its own source paths.
|
||||
// Source ranges can:
|
||||
// * refer to just an entire single line.
|
||||
// * can be a subset within a single line (when StartLine == EndLine)
|
||||
// * can also span multiple lines.
|
||||
// When column info is provided, the debugger will highlight the characters starting at the start line and start column,
|
||||
// and going up to but not including the character specified by the end line and end column.
|
||||
[Fx.Tag.SecurityNote(Miscellaneous = "RequiresReview - Our partial trust mechanisms require that this class remain Immutable. Do not add code that allows an instance of this class to change after creation without strict review.")]
|
||||
[DebuggerNonUserCode]
|
||||
[Serializable]
|
||||
[Fx.Tag.XamlVisible(false)]
|
||||
public class SourceLocation
|
||||
{
|
||||
string fileName;
|
||||
int startLine;
|
||||
int endLine;
|
||||
int startColumn;
|
||||
int endColumn;
|
||||
byte[] checksum;
|
||||
|
||||
// Define a source location from a filename and line-number (1-based).
|
||||
// This is a convenience constructor to specify the entire line.
|
||||
// This does not load the source file to determine column ranges.
|
||||
public SourceLocation(string fileName, int line)
|
||||
: this(fileName, line, 1, line, int.MaxValue)
|
||||
{
|
||||
}
|
||||
|
||||
public SourceLocation(
|
||||
string fileName,
|
||||
int startLine,
|
||||
int startColumn,
|
||||
int endLine,
|
||||
int endColumn)
|
||||
: this(fileName, null, startLine, startColumn, endLine, endColumn)
|
||||
{
|
||||
}
|
||||
|
||||
// Define a source location in a file.
|
||||
// Line/Column are 1-based.
|
||||
internal SourceLocation(
|
||||
string fileName,
|
||||
byte[] checksum,
|
||||
int startLine,
|
||||
int startColumn,
|
||||
int endLine,
|
||||
int endColumn)
|
||||
{
|
||||
if (startLine <= 0)
|
||||
{
|
||||
throw FxTrace.Exception.Argument("startLine", SR.InvalidSourceLocationLineNumber("startLine", startLine));
|
||||
}
|
||||
|
||||
if (startColumn <= 0)
|
||||
{
|
||||
throw FxTrace.Exception.Argument("startColumn", SR.InvalidSourceLocationColumn("startColumn", startColumn));
|
||||
}
|
||||
|
||||
if (endLine <= 0)
|
||||
{
|
||||
throw FxTrace.Exception.Argument("endLine", SR.InvalidSourceLocationLineNumber("endLine", endLine));
|
||||
}
|
||||
|
||||
if (endColumn <= 0)
|
||||
{
|
||||
throw FxTrace.Exception.Argument("endColumn", SR.InvalidSourceLocationColumn("endColumn", endColumn));
|
||||
}
|
||||
|
||||
if (startLine > endLine)
|
||||
{
|
||||
throw FxTrace.Exception.ArgumentOutOfRange("endLine", endLine, SR.OutOfRangeSourceLocationEndLine(startLine));
|
||||
}
|
||||
|
||||
if ((startLine == endLine) && (startColumn > endColumn))
|
||||
{
|
||||
throw FxTrace.Exception.ArgumentOutOfRange("endColumn", endColumn, SR.OutOfRangeSourceLocationEndColumn(startColumn));
|
||||
}
|
||||
|
||||
this.fileName = (fileName != null) ? fileName.ToUpperInvariant() : null;
|
||||
this.startLine = startLine;
|
||||
this.endLine = endLine;
|
||||
this.startColumn = startColumn;
|
||||
this.endColumn = endColumn;
|
||||
this.checksum = checksum;
|
||||
}
|
||||
|
||||
public string FileName
|
||||
{
|
||||
get { return this.fileName; }
|
||||
}
|
||||
|
||||
// Get the 1-based start line.
|
||||
public int StartLine
|
||||
{
|
||||
get { return this.startLine; }
|
||||
}
|
||||
|
||||
// Get the 1-based starting column.
|
||||
public int StartColumn
|
||||
{
|
||||
get { return this.startColumn; }
|
||||
}
|
||||
|
||||
// Get the 1-based end line. This should be greater or equal to StartLine.
|
||||
public int EndLine
|
||||
{
|
||||
get { return this.endLine; }
|
||||
}
|
||||
|
||||
// Get the 1-based ending column.
|
||||
public int EndColumn
|
||||
{
|
||||
get { return this.endColumn; }
|
||||
}
|
||||
|
||||
// get the checksum of the source file
|
||||
internal byte[] Checksum
|
||||
{
|
||||
get { return this.checksum; }
|
||||
}
|
||||
|
||||
public bool IsSingleWholeLine
|
||||
{
|
||||
get
|
||||
{
|
||||
return this.endColumn == int.MaxValue && this.startLine == this.endLine && this.startColumn == 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Equality comparison function. This checks for strict equality and
|
||||
// not for superset or subset relationships.
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
SourceLocation rsl = obj as SourceLocation;
|
||||
if (rsl == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.FileName != rsl.FileName)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.StartLine != rsl.StartLine ||
|
||||
this.StartColumn != rsl.StartColumn ||
|
||||
this.EndLine != rsl.EndLine ||
|
||||
this.EndColumn != rsl.EndColumn)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.Checksum == null ^ rsl.Checksum == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else if ((this.Checksum != null && rsl.Checksum != null) && !this.Checksum.SequenceEqual(rsl.Checksum))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// everything matches
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get a hash code.
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return (string.IsNullOrEmpty(this.FileName) ? 0 : this.FileName.GetHashCode()) ^
|
||||
this.StartLine.GetHashCode() ^
|
||||
this.StartColumn.GetHashCode() ^
|
||||
((this.Checksum == null) ? 0 : SymbolHelper.GetHexStringFromChecksum(this.Checksum).GetHashCode());
|
||||
}
|
||||
|
||||
internal static bool IsValidRange(int startLine, int startColumn, int endLine, int endColumn)
|
||||
{
|
||||
return
|
||||
(startLine > 0) && (startColumn > 0) && (endLine > 0) && (endColumn > 0) &&
|
||||
((startLine < endLine) || (startLine == endLine) && (startColumn < endColumn));
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,42 @@
|
||||
// <copyright>
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace System.Activities.Debugger
|
||||
{
|
||||
public sealed class SourceLocationFoundEventArgs : EventArgs
|
||||
{
|
||||
private object target;
|
||||
private SourceLocation sourceLocation;
|
||||
private bool isValueNode;
|
||||
|
||||
public SourceLocationFoundEventArgs(object target, SourceLocation sourceLocation)
|
||||
{
|
||||
UnitTestUtility.Assert(target != null, "Target cannot be null and is ensured by caller");
|
||||
UnitTestUtility.Assert(sourceLocation != null, "Target cannot be null and is ensured by caller");
|
||||
this.target = target;
|
||||
this.sourceLocation = sourceLocation;
|
||||
}
|
||||
|
||||
internal SourceLocationFoundEventArgs(object target, SourceLocation sourceLocation, bool isValueNode)
|
||||
: this(target, sourceLocation)
|
||||
{
|
||||
this.isValueNode = isValueNode;
|
||||
}
|
||||
|
||||
public object Target
|
||||
{
|
||||
get { return this.target; }
|
||||
}
|
||||
|
||||
public SourceLocation SourceLocation
|
||||
{
|
||||
get { return this.sourceLocation; }
|
||||
}
|
||||
|
||||
internal bool IsValueNode
|
||||
{
|
||||
get { return this.isValueNode; }
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,14 @@
|
||||
// <copyright>
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace System.Activities.Debugger
|
||||
{
|
||||
internal enum SourceLocationMemberType
|
||||
{
|
||||
StartLine,
|
||||
StartColumn,
|
||||
EndLine,
|
||||
EndColumn
|
||||
}
|
||||
}
|
@@ -0,0 +1,415 @@
|
||||
//----------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
//----------------------------------------------------------------
|
||||
|
||||
namespace System.Activities.Debugger
|
||||
{
|
||||
using System;
|
||||
using System.Activities.Hosting;
|
||||
using System.Activities.XamlIntegration;
|
||||
using System.Diagnostics;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime;
|
||||
using System.Reflection;
|
||||
using System.Security;
|
||||
using System.Security.Permissions;
|
||||
using System.Xaml;
|
||||
using System.Xml;
|
||||
using System.IO;
|
||||
using System.Activities.Validation;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Activities.Debugger.Symbol;
|
||||
using System.Globalization;
|
||||
|
||||
// Provide SourceLocation information for activities in given root activity.
|
||||
// This is integration point with Workflow project system (TBD).
|
||||
// The current plan is to get SourceLocation from (in this order):
|
||||
// 1. pdb (when available)
|
||||
// 2a. parse xaml files available in the same project (or metadata store) or
|
||||
// 2b. ask user to point to the correct xaml source.
|
||||
// 3. Publish (serialize to tmp file) and deserialize it to collect SourceLocation (for loose xaml).
|
||||
// Current code cover only step 3.
|
||||
|
||||
[DebuggerNonUserCode]
|
||||
public static class SourceLocationProvider
|
||||
{
|
||||
[Fx.Tag.Throws(typeof(Exception), "Calls Serialize/Deserialize to temporary file")]
|
||||
[SuppressMessage(FxCop.Category.Design, FxCop.Rule.DoNotCatchGeneralExceptionTypes,
|
||||
Justification = "We catch all exceptions to avoid leaking security sensitive information.")]
|
||||
[SuppressMessage(FxCop.Category.Security, "CA2103:ReviewImperativeSecurity",
|
||||
Justification = "This is security reviewed.")]
|
||||
[SuppressMessage(FxCop.Category.Security, FxCop.Rule.SecureAsserts,
|
||||
Justification = "The Assert is only enforce while reading the file and the contents is not leaked.")]
|
||||
[SuppressMessage("Reliability", "Reliability108:IsFatalRule",
|
||||
Justification = "We catch all exceptions to avoid leaking security sensitive information.")]
|
||||
[Fx.Tag.SecurityNote(Critical = "Asserting FileIOPermission(Read) for the specified file name that is contained the attached property on the XAML.",
|
||||
Safe = "We are not exposing the contents of the file.")]
|
||||
[SecuritySafeCritical]
|
||||
static internal Dictionary<object, SourceLocation> GetSourceLocations(Activity rootActivity, out string sourcePath, out bool isTemporaryFile, out byte[] checksum)
|
||||
{
|
||||
isTemporaryFile = false;
|
||||
checksum = null;
|
||||
string symbolString = DebugSymbol.GetSymbol(rootActivity) as String;
|
||||
if (string.IsNullOrEmpty(symbolString) && rootActivity.Children != null && rootActivity.Children.Count > 0)
|
||||
{ // In case of actual root is wrapped either in x:Class activity or CorrelationScope
|
||||
Activity body = rootActivity.Children[0];
|
||||
string bodySymbolString = DebugSymbol.GetSymbol(body) as String;
|
||||
if (!string.IsNullOrEmpty(bodySymbolString))
|
||||
{
|
||||
rootActivity = body;
|
||||
symbolString = bodySymbolString;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(symbolString))
|
||||
{
|
||||
try
|
||||
{
|
||||
WorkflowSymbol wfSymbol = WorkflowSymbol.Decode(symbolString);
|
||||
if (wfSymbol != null)
|
||||
{
|
||||
sourcePath = wfSymbol.FileName;
|
||||
checksum = wfSymbol.GetChecksum();
|
||||
// rootActivity is the activity with the attached symbol string.
|
||||
// rootActivity.RootActivity is the workflow root activity.
|
||||
// if they are not the same, then it must be compiled XAML, because loose XAML (i.e. XAMLX) always have the symbol attached at the root.
|
||||
if (rootActivity.RootActivity != rootActivity)
|
||||
{
|
||||
Fx.Assert(rootActivity.Parent != null, "Compiled XAML implementation always have a parent.");
|
||||
rootActivity = rootActivity.Parent;
|
||||
}
|
||||
return GetSourceLocations(rootActivity, wfSymbol, translateInternalActivityToOrigin: false);
|
||||
}
|
||||
}
|
||||
catch (SerializationException)
|
||||
{
|
||||
// Ignore invalid symbol.
|
||||
}
|
||||
}
|
||||
|
||||
sourcePath = XamlDebuggerXmlReader.GetFileName(rootActivity) as string;
|
||||
Dictionary<object, SourceLocation> mapping;
|
||||
Assembly localAssembly;
|
||||
bool permissionRevertNeeded = false;
|
||||
|
||||
// This may not be the local assembly since it may not be the real root for x:Class
|
||||
localAssembly = rootActivity.GetType().Assembly;
|
||||
|
||||
if (rootActivity.Parent != null)
|
||||
{
|
||||
localAssembly = rootActivity.Parent.GetType().Assembly;
|
||||
}
|
||||
|
||||
if (rootActivity.Children != null && rootActivity.Children.Count > 0)
|
||||
{ // In case of actual root is wrapped either in x:Class activity or CorrelationScope
|
||||
Activity body = rootActivity.Children[0];
|
||||
string bodySourcePath = XamlDebuggerXmlReader.GetFileName(body) as string;
|
||||
if (!string.IsNullOrEmpty(bodySourcePath))
|
||||
{
|
||||
rootActivity = body;
|
||||
sourcePath = bodySourcePath;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Fx.Assert(!string.IsNullOrEmpty(sourcePath), "If sourcePath is null, it should have been short-circuited before reaching here.");
|
||||
|
||||
SourceLocation tempSourceLocation;
|
||||
Activity tempRootActivity;
|
||||
|
||||
checksum = SymbolHelper.CalculateChecksum(sourcePath);
|
||||
|
||||
if (TryGetSourceLocation(rootActivity, sourcePath, checksum, out tempSourceLocation)) // already has source location.
|
||||
{
|
||||
tempRootActivity = rootActivity;
|
||||
}
|
||||
else
|
||||
{
|
||||
byte[] buffer;
|
||||
// Need to store the file in memory temporary so don't have to re-read the file twice
|
||||
// for XamlDebugXmlReader's BracketLocator.
|
||||
// If there is a debugger attached, Assert FileIOPermission for Read access to the specific file.
|
||||
if (System.Diagnostics.Debugger.IsAttached)
|
||||
{
|
||||
permissionRevertNeeded = true;
|
||||
FileIOPermission permission = new FileIOPermission(FileIOPermissionAccess.Read, sourcePath);
|
||||
permission.Assert();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
FileInfo fi = new FileInfo(sourcePath);
|
||||
buffer = new byte[fi.Length];
|
||||
|
||||
using (FileStream fs = new FileStream(sourcePath, FileMode.Open, FileAccess.Read))
|
||||
{
|
||||
fs.Read(buffer, 0, buffer.Length);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
// If we Asserted FileIOPermission, revert it.
|
||||
if (permissionRevertNeeded)
|
||||
{
|
||||
CodeAccessPermission.RevertAssert();
|
||||
permissionRevertNeeded = false;
|
||||
}
|
||||
}
|
||||
|
||||
object deserializedObject = Deserialize(buffer, localAssembly);
|
||||
IDebuggableWorkflowTree debuggableWorkflowTree = deserializedObject as IDebuggableWorkflowTree;
|
||||
if (debuggableWorkflowTree != null)
|
||||
{ // Declarative Service and x:Class case
|
||||
tempRootActivity = debuggableWorkflowTree.GetWorkflowRoot();
|
||||
}
|
||||
else
|
||||
{ // Loose XAML case.
|
||||
tempRootActivity = deserializedObject as Activity;
|
||||
}
|
||||
|
||||
Fx.Assert(tempRootActivity != null, "Unexpected workflow xaml file");
|
||||
}
|
||||
|
||||
mapping = new Dictionary<object, SourceLocation>();
|
||||
if (tempRootActivity != null)
|
||||
{
|
||||
CollectMapping(rootActivity, tempRootActivity, mapping, sourcePath, checksum);
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Only eat the exception if we were running in partial trust.
|
||||
if (!PartialTrustHelpers.AppDomainFullyTrusted)
|
||||
{
|
||||
// Eat the exception and return an empty dictionary.
|
||||
return new Dictionary<object, SourceLocation>();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
return mapping;
|
||||
}
|
||||
|
||||
public static Dictionary<object, SourceLocation> GetSourceLocations(Activity rootActivity, WorkflowSymbol symbol)
|
||||
{
|
||||
return GetSourceLocations(rootActivity, symbol, translateInternalActivityToOrigin: true);
|
||||
}
|
||||
|
||||
// For most of the time, we need source location for object that appear on XAML.
|
||||
// During debugging, however, we must not transform the internal activity to their origin to make sure it stop when the internal activity is about the execute
|
||||
// Therefore, in debugger scenario, translateInternalActivityToOrigin will be set to false.
|
||||
internal static Dictionary<object, SourceLocation> GetSourceLocations(Activity rootActivity, WorkflowSymbol symbol, bool translateInternalActivityToOrigin)
|
||||
{
|
||||
Activity workflowRoot = rootActivity.RootActivity ?? rootActivity;
|
||||
if (!workflowRoot.IsMetadataFullyCached)
|
||||
{
|
||||
IList<ValidationError> validationErrors = null;
|
||||
ActivityUtilities.CacheRootMetadata(workflowRoot, new ActivityLocationReferenceEnvironment(), ProcessActivityTreeOptions.ValidationOptions, null, ref validationErrors);
|
||||
}
|
||||
|
||||
Dictionary<object, SourceLocation> newMapping = new Dictionary<object, SourceLocation>();
|
||||
|
||||
// Make sure the qid we are using to TryGetElementFromRoot
|
||||
// are shifted appropriately such that the first digit that QID is
|
||||
// the same as the last digit of the rootActivity.QualifiedId.
|
||||
|
||||
int[] rootIdArray = rootActivity.QualifiedId.AsIDArray();
|
||||
int idOffset = rootIdArray[rootIdArray.Length - 1] - 1;
|
||||
|
||||
foreach (ActivitySymbol actSym in symbol.Symbols)
|
||||
{
|
||||
QualifiedId qid = new QualifiedId(actSym.QualifiedId);
|
||||
if (idOffset != 0)
|
||||
{
|
||||
int[] idArray = qid.AsIDArray();
|
||||
idArray[0] += idOffset;
|
||||
qid = new QualifiedId(idArray);
|
||||
}
|
||||
Activity activity;
|
||||
if (QualifiedId.TryGetElementFromRoot(rootActivity, qid, out activity))
|
||||
{
|
||||
object origin = activity;
|
||||
if (translateInternalActivityToOrigin && activity.Origin != null)
|
||||
{
|
||||
origin = activity.Origin;
|
||||
}
|
||||
|
||||
newMapping.Add(origin,
|
||||
new SourceLocation(symbol.FileName, symbol.GetChecksum(), actSym.StartLine, actSym.StartColumn, actSym.EndLine, actSym.EndColumn));
|
||||
}
|
||||
}
|
||||
return newMapping;
|
||||
}
|
||||
|
||||
[Fx.Tag.SecurityNote(Miscellaneous = "RequiresReview - We are deserializing XAML from a file. The file may have been read under and Assert for FileIOPermission. The data hould be validated and not cached.")]
|
||||
internal static object Deserialize(byte[] buffer, Assembly localAssembly)
|
||||
{
|
||||
using (MemoryStream memoryStream = new MemoryStream(buffer))
|
||||
{
|
||||
using (TextReader streamReader = new StreamReader(memoryStream))
|
||||
{
|
||||
using (XamlDebuggerXmlReader xamlDebuggerReader = new XamlDebuggerXmlReader(streamReader, new XamlSchemaContext(), localAssembly))
|
||||
{
|
||||
xamlDebuggerReader.SourceLocationFound += XamlDebuggerXmlReader.SetSourceLocation;
|
||||
|
||||
using (XamlReader activityBuilderReader = ActivityXamlServices.CreateBuilderReader(xamlDebuggerReader))
|
||||
{
|
||||
return XamlServices.Load(activityBuilderReader);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void CollectMapping(Activity rootActivity1, Activity rootActivity2, Dictionary<object, SourceLocation> mapping, string path)
|
||||
{
|
||||
CollectMapping(rootActivity1, rootActivity2, mapping, path, null, requirePrepareForRuntime: true);
|
||||
}
|
||||
|
||||
// Collect mapping for activity1 and its descendants to their corresponding source location.
|
||||
// activity2 is the shadow of activity1 but with SourceLocation information.
|
||||
[Fx.Tag.SecurityNote(Miscellaneous = "RequiresReview - We are dealing with activity and SourceLocation information that came from the user, possibly under an Assert for FileIOPermission. The data hould be validated and not cached.")]
|
||||
static void CollectMapping(Activity rootActivity1, Activity rootActivity2, Dictionary<object, SourceLocation> mapping, string path, byte[] checksum, bool requirePrepareForRuntime)
|
||||
{
|
||||
// For x:Class, the rootActivity here may not be the real root, but it's the first child of the x:Class activity.
|
||||
Activity realRoot1 = (rootActivity1.RootActivity != null) ? rootActivity1.RootActivity : rootActivity1;
|
||||
if ((requirePrepareForRuntime && !realRoot1.IsRuntimeReady) || (!requirePrepareForRuntime && !realRoot1.IsMetadataFullyCached))
|
||||
{
|
||||
IList<ValidationError> validationErrors = null;
|
||||
ActivityUtilities.CacheRootMetadata(realRoot1, new ActivityLocationReferenceEnvironment(), ProcessActivityTreeOptions.ValidationOptions, null, ref validationErrors);
|
||||
}
|
||||
|
||||
// Similarly for rootActivity2.
|
||||
Activity realRoot2 = (rootActivity2.RootActivity != null) ? rootActivity2.RootActivity : rootActivity2;
|
||||
if (rootActivity1 != rootActivity2 && (requirePrepareForRuntime && !realRoot2.IsRuntimeReady) || (!requirePrepareForRuntime && !realRoot2.IsMetadataFullyCached))
|
||||
{
|
||||
IList<ValidationError> validationErrors = null;
|
||||
ActivityUtilities.CacheRootMetadata(realRoot2, new ActivityLocationReferenceEnvironment(), ProcessActivityTreeOptions.ValidationOptions, null, ref validationErrors);
|
||||
}
|
||||
|
||||
Queue<KeyValuePair<Activity, Activity>> pairsRemaining = new Queue<KeyValuePair<Activity, Activity>>();
|
||||
|
||||
pairsRemaining.Enqueue(new KeyValuePair<Activity, Activity>(rootActivity1, rootActivity2));
|
||||
KeyValuePair<Activity, Activity> currentPair;
|
||||
HashSet<Activity> visited = new HashSet<Activity>();
|
||||
|
||||
while (pairsRemaining.Count > 0)
|
||||
{
|
||||
currentPair = pairsRemaining.Dequeue();
|
||||
Activity activity1 = currentPair.Key;
|
||||
Activity activity2 = currentPair.Value;
|
||||
|
||||
visited.Add(activity1);
|
||||
|
||||
SourceLocation sourceLocation;
|
||||
if (TryGetSourceLocation(activity2, path, checksum, out sourceLocation))
|
||||
{
|
||||
mapping.Add(activity1, sourceLocation);
|
||||
}
|
||||
else if (!((activity2 is IExpressionContainer) || (activity2 is IValueSerializableExpression))) // Expression is known not to have source location.
|
||||
{
|
||||
//Some activities may not have corresponding Xaml node, e.g. ActivityFaultedOutput.
|
||||
Trace.WriteLine("WorkflowDebugger: Does not have corresponding Xaml node for: " + activity2.DisplayName + "\n");
|
||||
}
|
||||
|
||||
// This to avoid comparing any value expression with DesignTimeValueExpression (in designer case).
|
||||
if (!((activity1 is IExpressionContainer) || (activity2 is IExpressionContainer) ||
|
||||
(activity1 is IValueSerializableExpression) || (activity2 is IValueSerializableExpression)))
|
||||
{
|
||||
IEnumerator<Activity> enumerator1 = WorkflowInspectionServices.GetActivities(activity1).GetEnumerator();
|
||||
IEnumerator<Activity> enumerator2 = WorkflowInspectionServices.GetActivities(activity2).GetEnumerator();
|
||||
bool hasNextItem1 = enumerator1.MoveNext();
|
||||
bool hasNextItem2 = enumerator2.MoveNext();
|
||||
while (hasNextItem1 && hasNextItem2)
|
||||
{
|
||||
if (!visited.Contains(enumerator1.Current)) // avoid adding the same activity (e.g. some default implementation).
|
||||
{
|
||||
if (enumerator1.Current.GetType() != enumerator2.Current.GetType())
|
||||
{
|
||||
// Give debugger log instead of just asserting; to help user find out mismatch problem.
|
||||
Trace.WriteLine(
|
||||
"Unmatched type: " + enumerator1.Current.GetType().FullName +
|
||||
" vs " + enumerator2.Current.GetType().FullName + "\n");
|
||||
}
|
||||
pairsRemaining.Enqueue(new KeyValuePair<Activity, Activity>(enumerator1.Current, enumerator2.Current));
|
||||
}
|
||||
hasNextItem1 = enumerator1.MoveNext();
|
||||
hasNextItem2 = enumerator2.MoveNext();
|
||||
}
|
||||
|
||||
// If enumerators do not finish at the same time, then they have unmatched number of activities.
|
||||
// Give debugger log instead of just asserting; to help user find out mismatch problem.
|
||||
if (hasNextItem1 || hasNextItem2)
|
||||
{
|
||||
Trace.WriteLine("Unmatched number of children\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void CollectMapping(Activity rootActivity1, Activity rootActivity2, Dictionary<object, SourceLocation> mapping, string path, byte[] checksum)
|
||||
{
|
||||
CollectMapping(rootActivity1, rootActivity2, mapping, path, checksum, requirePrepareForRuntime: true);
|
||||
}
|
||||
// Get SourceLocation for object deserialized with XamlDebuggerXmlReader in deserializer stack.
|
||||
static bool TryGetSourceLocation(object obj, string path, byte[] checksum, out SourceLocation sourceLocation)
|
||||
{
|
||||
sourceLocation = null;
|
||||
int startLine, startColumn, endLine, endColumn;
|
||||
|
||||
if (AttachablePropertyServices.TryGetProperty<int>(obj, XamlDebuggerXmlReader.StartLineName, out startLine) &&
|
||||
AttachablePropertyServices.TryGetProperty<int>(obj, XamlDebuggerXmlReader.StartColumnName, out startColumn) &&
|
||||
AttachablePropertyServices.TryGetProperty<int>(obj, XamlDebuggerXmlReader.EndLineName, out endLine) &&
|
||||
AttachablePropertyServices.TryGetProperty<int>(obj, XamlDebuggerXmlReader.EndColumnName, out endColumn) &&
|
||||
SourceLocation.IsValidRange(startLine, startColumn, endLine, endColumn))
|
||||
{
|
||||
sourceLocation = new SourceLocation(path, checksum, startLine, startColumn, endLine, endColumn);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static ICollection<ActivitySymbol> GetSymbols(Activity rootActivity, Dictionary<object, SourceLocation> sourceLocations)
|
||||
{
|
||||
List<ActivitySymbol> symbols = new List<ActivitySymbol>();
|
||||
Activity realRoot = (rootActivity.RootActivity != null) ? rootActivity.RootActivity : rootActivity;
|
||||
if (!realRoot.IsMetadataFullyCached)
|
||||
{
|
||||
IList<ValidationError> validationErrors = null;
|
||||
ActivityUtilities.CacheRootMetadata(realRoot, new ActivityLocationReferenceEnvironment(), ProcessActivityTreeOptions.ValidationOptions, null, ref validationErrors);
|
||||
}
|
||||
Queue<Activity> activitiesRemaining = new Queue<Activity>();
|
||||
activitiesRemaining.Enqueue(realRoot);
|
||||
HashSet<Activity> visited = new HashSet<Activity>();
|
||||
while (activitiesRemaining.Count > 0)
|
||||
{
|
||||
Activity currentActivity = activitiesRemaining.Dequeue();
|
||||
SourceLocation sourceLocation;
|
||||
object origin = currentActivity.Origin == null ? currentActivity : currentActivity.Origin;
|
||||
if (!visited.Contains(currentActivity) && sourceLocations.TryGetValue(origin, out sourceLocation))
|
||||
{
|
||||
symbols.Add(new ActivitySymbol
|
||||
{
|
||||
QualifiedId = currentActivity.QualifiedId.AsByteArray(),
|
||||
StartLine = sourceLocation.StartLine,
|
||||
StartColumn = sourceLocation.StartColumn,
|
||||
EndLine = sourceLocation.EndLine,
|
||||
EndColumn = sourceLocation.EndColumn
|
||||
});
|
||||
}
|
||||
visited.Add(currentActivity);
|
||||
foreach (Activity childActivity in WorkflowInspectionServices.GetActivities(currentActivity))
|
||||
{
|
||||
activitiesRemaining.Enqueue(childActivity);
|
||||
}
|
||||
}
|
||||
return symbols;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,259 @@
|
||||
//-----------------------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
namespace System.Activities.Debugger
|
||||
{
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using System.Runtime;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Security;
|
||||
using System.Text;
|
||||
using System.IO;
|
||||
using System.Globalization;
|
||||
|
||||
// Describes a "state" in the interpretter. A state is any source location that
|
||||
// a breakpoint could be set on or that could be stepped to.
|
||||
[DebuggerNonUserCode]
|
||||
[Fx.Tag.XamlVisible(false)]
|
||||
public class State
|
||||
{
|
||||
[Fx.Tag.SecurityNote(Critical = "This value is used in IL generation performed under an assert. It gets validated before setting in partial trust.")]
|
||||
[SecurityCritical]
|
||||
SourceLocation location;
|
||||
[Fx.Tag.SecurityNote(Critical = "This value is used in IL generation performed under an assert. It gets validated before setting in partial trust.")]
|
||||
[SecurityCritical]
|
||||
string name;
|
||||
IEnumerable<LocalsItemDescription> earlyLocals;
|
||||
int numberOfEarlyLocals;
|
||||
|
||||
// Calling Type.GetMethod() is slow (10,000 calls can take ~1 minute).
|
||||
// So we stash extra fields to be able to make the call lazily (as we Enter the state).
|
||||
// this.type.GetMethod
|
||||
Type type;
|
||||
[Fx.Tag.SecurityNote(Critical = "This value is used in IL generation performed under an assert. It gets validated before setting in partial trust.")]
|
||||
[SecurityCritical]
|
||||
string methodName;
|
||||
|
||||
[Fx.Tag.SecurityNote(Critical = "This value is used in IL generation performed under an assert. Used to determine if we should invoke the generated code for this state.")]
|
||||
[SecurityCritical]
|
||||
bool debuggingEnabled = true;
|
||||
|
||||
[Fx.Tag.SecurityNote(Critical = "Sets SecurityCritical name member.",
|
||||
Safe = "We validate the SourceLocation and name before storing it in the member when running in Partial Trust.")]
|
||||
[SecuritySafeCritical]
|
||||
internal State(SourceLocation location, string name, IEnumerable<LocalsItemDescription> earlyLocals, int numberOfEarlyLocals)
|
||||
{
|
||||
// If we are running in Partial Trust, validate the name string. We only do this in partial trust for backward compatability.
|
||||
// We are doing the validation because we want to prevent anything passed to us by non-critical code from affecting the generation
|
||||
// of the code to the dynamic assembly we are creating.
|
||||
if (!PartialTrustHelpers.AppDomainFullyTrusted)
|
||||
{
|
||||
this.name = ValidateIdentifierString(name);
|
||||
this.location = ValidateSourceLocation(location);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.location = location;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
this.earlyLocals = earlyLocals;
|
||||
Fx.Assert(earlyLocals != null || numberOfEarlyLocals == 0,
|
||||
"If earlyLocals is null then numberOfEarlyLocals should be 0");
|
||||
// Ignore the passed numberOfEarlyLocals if earlyLocal is null.
|
||||
this.numberOfEarlyLocals = (earlyLocals == null) ? 0 : numberOfEarlyLocals;
|
||||
}
|
||||
|
||||
// Location in source file associated with this state.
|
||||
internal SourceLocation Location
|
||||
{
|
||||
[Fx.Tag.SecurityNote(Critical = "Accesses the SecurityCritical location member. We validated the location when this object was constructed.",
|
||||
Safe = "SourceLocation is immutable and we validated it in the constructor.")]
|
||||
[SecuritySafeCritical]
|
||||
get { return this.location; }
|
||||
}
|
||||
|
||||
|
||||
// Friendly name of the state. May be null if state is not named.
|
||||
// States need unique names.
|
||||
internal string Name
|
||||
{
|
||||
[Fx.Tag.SecurityNote(Critical = "Sets SecurityCritical name member.",
|
||||
Safe = "We are only reading it, not setting it.")]
|
||||
[SecuritySafeCritical]
|
||||
get { return this.name; }
|
||||
}
|
||||
|
||||
|
||||
// Type definitions for early bound locals. This list is ordered.
|
||||
// Names should be unique.
|
||||
internal IEnumerable<LocalsItemDescription> EarlyLocals
|
||||
{
|
||||
get { return this.earlyLocals; }
|
||||
}
|
||||
|
||||
internal int NumberOfEarlyLocals
|
||||
{
|
||||
get { return this.numberOfEarlyLocals; }
|
||||
}
|
||||
|
||||
internal bool DebuggingEnabled
|
||||
{
|
||||
[Fx.Tag.SecurityNote(Critical = "Accesses SecurityCritical debuggingEnabled member.",
|
||||
Safe = "We don't change anyting. We only return the value.")]
|
||||
[SecuritySafeCritical]
|
||||
get
|
||||
{
|
||||
return this.debuggingEnabled;
|
||||
}
|
||||
|
||||
[Fx.Tag.SecurityNote(Critical = "Sets SecurityCritical debuggingEnabled member.")]
|
||||
[SecuritySafeCritical]
|
||||
set
|
||||
{
|
||||
this.debuggingEnabled = value;
|
||||
}
|
||||
}
|
||||
|
||||
[Fx.Tag.SecurityNote(Critical = "Sets SecurityCritical methodName member.")]
|
||||
[SecurityCritical]
|
||||
internal void CacheMethodInfo(Type type, string methodName)
|
||||
{
|
||||
this.type = type;
|
||||
this.methodName = methodName;
|
||||
}
|
||||
|
||||
// Helper to lazily get the MethodInfo. This is expensive, so caller should cache it.
|
||||
[Fx.Tag.SecurityNote(Critical = "Generates and returns a MethodInfo that is used to generate the dynamic module and accesses Critical member methodName.")]
|
||||
[SecurityCritical]
|
||||
internal MethodInfo GetMethodInfo(bool withPriming)
|
||||
{
|
||||
MethodInfo methodInfo = this.type.GetMethod(withPriming ? StateManager.MethodWithPrimingPrefix + this.methodName : this.methodName);
|
||||
return methodInfo;
|
||||
}
|
||||
|
||||
// internal because it is used from StateManager, too for the assembly name, type name, and type name prefix.
|
||||
internal static string ValidateIdentifierString(string input)
|
||||
{
|
||||
string result = input.Normalize(NormalizationForm.FormC);
|
||||
|
||||
if (result.Length > 255)
|
||||
{
|
||||
result = result.Substring(0, 255);
|
||||
}
|
||||
|
||||
// Make the identifier conform to Unicode programming language identifer specification.
|
||||
char[] chars = result.ToCharArray();
|
||||
for (int i = 0; i < chars.Length; i++)
|
||||
{
|
||||
UnicodeCategory category = char.GetUnicodeCategory(chars[i]);
|
||||
// Check for identifier_start
|
||||
if ((category == UnicodeCategory.UppercaseLetter) ||
|
||||
(category == UnicodeCategory.LowercaseLetter) ||
|
||||
(category == UnicodeCategory.TitlecaseLetter) ||
|
||||
(category == UnicodeCategory.ModifierLetter) ||
|
||||
(category == UnicodeCategory.OtherLetter) ||
|
||||
(category == UnicodeCategory.LetterNumber))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// If it's not the first character, also check for identifier_extend
|
||||
if ((i != 0) &&
|
||||
((category == UnicodeCategory.NonSpacingMark) ||
|
||||
(category == UnicodeCategory.SpacingCombiningMark) ||
|
||||
(category == UnicodeCategory.DecimalDigitNumber) ||
|
||||
(category == UnicodeCategory.ConnectorPunctuation) ||
|
||||
(category == UnicodeCategory.Format)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Not valid for identifiers - change it to an underscore.
|
||||
chars[i] = '_';
|
||||
}
|
||||
|
||||
result = new string(chars);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
[Fx.Tag.SecurityNote(Critical = "Calls SecurityCritical method StateManager.DisableCodeGeneration.")]
|
||||
[SecurityCritical]
|
||||
SourceLocation ValidateSourceLocation(SourceLocation input)
|
||||
{
|
||||
bool returnNewLocation = false;
|
||||
string newFileName = input.FileName;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(newFileName))
|
||||
{
|
||||
this.DebuggingEnabled = false;
|
||||
Trace.WriteLine(SR.DebugInstrumentationFailed(SR.InvalidFileName(this.name)));
|
||||
return input;
|
||||
}
|
||||
|
||||
// There was some validation of the column and line number already done in the SourceLocation constructor.
|
||||
// We are going to limit line and column numbers to Int16.MaxValue
|
||||
if ((input.StartLine > Int16.MaxValue) || (input.EndLine > Int16.MaxValue))
|
||||
{
|
||||
this.DebuggingEnabled = false;
|
||||
Trace.WriteLine(SR.DebugInstrumentationFailed(SR.LineNumberTooLarge(this.name)));
|
||||
return input;
|
||||
}
|
||||
|
||||
if ((input.StartColumn > Int16.MaxValue) || (input.EndColumn > Int16.MaxValue))
|
||||
{
|
||||
this.DebuggingEnabled = false;
|
||||
Trace.WriteLine(SR.DebugInstrumentationFailed(SR.ColumnNumberTooLarge(this.name)));
|
||||
return input;
|
||||
}
|
||||
|
||||
// Truncate at 255 characters.
|
||||
if (newFileName.Length > 255)
|
||||
{
|
||||
newFileName = newFileName.Substring(0, 255);
|
||||
returnNewLocation = true;
|
||||
}
|
||||
|
||||
if (ReplaceInvalidCharactersWithUnderscore(ref newFileName, Path.GetInvalidPathChars()))
|
||||
{
|
||||
returnNewLocation = true;
|
||||
}
|
||||
|
||||
string fileNameOnly = Path.GetFileName(newFileName);
|
||||
if (ReplaceInvalidCharactersWithUnderscore(ref fileNameOnly, Path.GetInvalidFileNameChars()))
|
||||
{
|
||||
// The filename portion has been munged. We need to make a new full name.
|
||||
string path = Path.GetDirectoryName(newFileName);
|
||||
newFileName = path + "\\" + fileNameOnly;
|
||||
returnNewLocation = true;
|
||||
}
|
||||
|
||||
if (returnNewLocation)
|
||||
{
|
||||
return new SourceLocation(newFileName, input.StartLine, input.StartColumn, input.EndLine, input.EndColumn);
|
||||
}
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
static bool ReplaceInvalidCharactersWithUnderscore(ref string input, char[] invalidChars)
|
||||
{
|
||||
bool modified = false;
|
||||
int invalidIndex = 0;
|
||||
while ((invalidIndex = input.IndexOfAny(invalidChars)) != -1)
|
||||
{
|
||||
char[] charArray = input.ToCharArray();
|
||||
charArray[invalidIndex] = '_';
|
||||
input = new string(charArray);
|
||||
modified = true;
|
||||
|
||||
}
|
||||
|
||||
return modified;
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user