Skip to content

Commit

Permalink
Merge pull request #76 from delegateas/assembly-not-updated
Browse files Browse the repository at this point in the history
Compare version between local and registered assembly and change hash function.
  • Loading branch information
mkholt authored Apr 25, 2024
2 parents 29d0d76 + b7eb9e2 commit a27098a
Show file tree
Hide file tree
Showing 12 changed files with 89 additions and 97 deletions.
14 changes: 9 additions & 5 deletions src/Delegate.Daxif/API/Plugin.fs
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,29 @@ open DG.Daxif.Modules.Plugin
module SolutionMain = DG.Daxif.Modules.Solution.Main

type Plugin private () =

/// <summary>Updates plugin registrations in CRM based on the plugins found in your local assembly.</summary>
/// <param name="env">Environment the action should be performed against.</param>
/// <param name="assemblyPath">Path to the plugin assembly dll to be synced (usually under the project bin folder).</param>
/// <param name="projectPath">Path to the plugin assembly project (.csproj).</param>
/// <param name="projectPath">DEPRECATED: PASS AN EMPTY STRING. Path to the plugin assembly project (.csproj).</param>
/// <param name="solutionName">The name of the solution to which to sync plugins</param>
/// <param name="dryRun">Flag whether or not to simulate/test syncing plugins (running a 'dry run'). - defaults to: false</param>
/// <param name="isolationMode">Assembly Isolation Mode ('Sandbox' or 'None'). All Online environments must use 'Sandbox' - defaults to: 'Sandbox'</param>
/// <param name="ignoreOutdatedAssembly">Flag whether or not to simulate/test syncing plugins (running a 'dry run'). - defaults to: false</param>
/// <param name="ignoreOutdatedAssembly">DEPRECATED. Flag whether or not to simulate/test syncing plugins (running a 'dry run'). - defaults to: false</param>
/// <param name="logLevel">Log Level - Error, Warning, Info, Verbose or Debug - defaults to: 'Verbose'</param>
static member Sync(env: Environment, assemblyPath: string, projectPath: string, solutionName: string, ?dryRun: bool, ?isolationMode: AssemblyIsolationMode, ?ignoreOutdatedAssembly: bool, ?logLevel: LogLevel) =
if (projectPath <> "") then
log.Warn "The 'projectPath' parameter is deprecated and will be removed in a future version. Please remove it from your code. (Pass an empty string to silence this warning)"

if (ignoreOutdatedAssembly.IsSome) then
log.Warn "The 'ignoreOutdatedAssembly' parameter is deprecated and will be removed in a future version. Please remove it from your code."

let proxyGen = env.connect(log).GetService
log.setLevelOption logLevel

let dryRun = dryRun ?| false
let isolationMode = isolationMode ?| AssemblyIsolationMode.Sandbox
let ignoreOutdatedAssembly = ignoreOutdatedAssembly ?| false

Main.syncSolution proxyGen projectPath assemblyPath solutionName isolationMode ignoreOutdatedAssembly dryRun |> ignore
Main.syncSolution proxyGen assemblyPath solutionName isolationMode dryRun |> ignore

/// <summary>Activates or deactivates all plugin steps of a solution</summary>
/// <param name="env">Environment the action should be performed against.</param>
Expand Down
8 changes: 4 additions & 4 deletions src/Delegate.Daxif/AssemblyInfo.fs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ open System.Reflection
[<assembly: AssemblyDescriptionAttribute("Delegate Automated xRM Installation Framework")>]
[<assembly: AssemblyCompanyAttribute("Delegate")>]
[<assembly: AssemblyCopyrightAttribute("Copyright (c) Delegate A/S 2017")>]
[<assembly: AssemblyVersionAttribute("5.5.1")>]
[<assembly: AssemblyFileVersionAttribute("5.5.1")>]
[<assembly: AssemblyVersionAttribute("5.6.0")>]
[<assembly: AssemblyFileVersionAttribute("5.6.0")>]
do ()

module internal AssemblyVersionInformation =
Expand All @@ -17,5 +17,5 @@ module internal AssemblyVersionInformation =
let [<Literal>] AssemblyDescription = "Delegate Automated xRM Installation Framework"
let [<Literal>] AssemblyCompany = "Delegate"
let [<Literal>] AssemblyCopyright = "Copyright (c) Delegate A/S 2017"
let [<Literal>] AssemblyVersion = "5.5.1"
let [<Literal>] AssemblyFileVersion = "5.5.1"
let [<Literal>] AssemblyVersion = "5.6.0"
let [<Literal>] AssemblyFileVersion = "5.6.0"
3 changes: 3 additions & 0 deletions src/Delegate.Daxif/Common/Utility.fs
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,9 @@ let parseVersion (str:string): Version =
let getIdx idx = Array.tryItem idx vArr ?>> parseInt ?| 0
(getIdx 0, getIdx 1, getIdx 2, getIdx 3)

let versionToString (version:Version): string =
let (a,b,c,d) = version
sprintf "%d.%d.%d.%d" a b c d

let getIntGroup def (m:Match) (idx:int) = parseInt m.Groups.[idx].Value ?| def
let getMinVersion = getIntGroup 0
Expand Down
5 changes: 5 additions & 0 deletions src/Delegate.Daxif/Daxif.fs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ type AsyncJobState =
| Failed = 31
| Canceled = 32

type AssemblyOperation =
| Unchanged
| Create
| Update

type Version = int * int * int * int
type VersionCriteria = Version option * Version option

Expand Down
20 changes: 17 additions & 3 deletions src/Delegate.Daxif/Modules/Plugins/Compare.fs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

open System
open Microsoft.Xrm.Sdk
open DG.Daxif
open DG.Daxif.Common
open DG.Daxif.Common.Utility

Expand Down Expand Up @@ -135,9 +136,22 @@ let image (img: Image) (x: Entity) =
/// Compares a Custom API Response Property from CRM with one in source code
// TODO


/// Compares an assembly from CRM with the one containing the source code
let assembly (local: AssemlyLocal) (registered: AssemblyRegistration option) =
/// Returns true if the assembly in CRM is newer and the hash matches the one in the source code
let registeredIsSameAsLocal (local: AssemlyLocal) (registered: AssemblyRegistration option) =
registered
?|> fun y -> y.hash = local.hash
?|> fun y ->
let log = ConsoleLogger.Global

let environmentIsNewer = y.version .>= local.version
log.Verbose "Registered version %s is %s than local version %s"
(y.version |> versionToString) (if environmentIsNewer then "newer" else "older") (local.version |> versionToString)

let hashMatch = y.hash = local.hash
log.Verbose "Registered assembly hash %s local assembly hash" (if hashMatch then "matches" else "does not match")

let isSameAssembly = environmentIsNewer && hashMatch
log.Verbose "Assembly will%s be updated" (if isSameAssembly then " not" else "")

isSameAssembly
?| false
4 changes: 4 additions & 0 deletions src/Delegate.Daxif/Modules/Plugins/Domain.fs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ open System
open System.Reflection
open Microsoft.Xrm.Sdk
open DG.Daxif
open DG.Daxif.Common

(** Enum for plugin configurations **)
type ExecutionMode =
Expand Down Expand Up @@ -144,6 +145,7 @@ type AssemlyLocal =
dllName: String
dllPath: String
hash: String
version: Version
isolationMode: AssemblyIsolationMode
plugins: Plugin seq
customAPIs: CustomAPI seq
Expand All @@ -152,9 +154,11 @@ type AssemlyLocal =
type AssemblyRegistration = {
id: Guid
hash: String
version: Version
} with
static member fromEntity (e:Entity) =
{
id = e.Id
hash = e.GetAttributeValue<string>("sourcehash")
version = e.GetAttributeValue<string>("version") |> Utility.parseVersion
}
9 changes: 6 additions & 3 deletions src/Delegate.Daxif/Modules/Plugins/Main.fs
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,19 @@

open DG.Daxif
open DG.Daxif.Modules.Plugin
open DG.Daxif.Common
open DG.Daxif.Common.Utility
open DG.Daxif.Common.InternalUtility

open Domain


/// Main plugin synchronization function
let syncSolution proxyGen projectPath dllPath solutionName isolationMode ignoreOutdatedAssembly dryRun =
let syncSolution proxyGen dllPath solutionName isolationMode dryRun =
logVersion log
log.Info "Action: Plugin synchronization"

log.Info "Comparing plugins registered in CRM versus those found in your local code"
let asmLocal, asmReg, pluginsLocal, pluginsReg, prefix = MainHelper.analyze proxyGen projectPath dllPath solutionName isolationMode ignoreOutdatedAssembly
let asmLocal, asmReg, pluginsLocal, pluginsReg, prefix = MainHelper.analyze proxyGen dllPath solutionName isolationMode

match dryRun with
| false ->
Expand All @@ -25,6 +24,10 @@ let syncSolution proxyGen projectPath dllPath solutionName isolationMode ignoreO
log.Info "***** Dry run *****"
let regTypes, regSteps, regImages, regCustomApis, regReqParams, regRespParams = pluginsReg
let localTypes, localSteps, localImages, localCustomApiTypes, localCustomApis, localReqParams, localRespParams = pluginsLocal
match MainHelper.determineOperation asmReg asmLocal with
| Unchanged, _ -> log.Info "No changes detected to assembly"
| Create, _ -> log.Info "Would create new assembly"
| Update, _ -> log.Info "Would update assembly"
printMergePartition "Types" localTypes regTypes Compare.pluginType log
printMergePartition "Steps" localSteps regSteps Compare.step log
printMergePartition "Images" localImages regImages Compare.image log
Expand Down
43 changes: 25 additions & 18 deletions src/Delegate.Daxif/Modules/Plugins/MainHelper.fs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ open System
open Microsoft.Xrm.Sdk
open Microsoft.Xrm.Sdk.Messages

open DG.Daxif
open DG.Daxif.Common
open DG.Daxif.Common.Utility

Expand Down Expand Up @@ -61,21 +62,27 @@ let localToMaps (plugins: Plugin seq) (customAPIs: CustomAPI seq) =

mergedTypeMap, stepMap, imageMap, customApiTypeMap, customApiMap, reqParamMap, respPropMap

/// Determine which operation we want to perform on the assembly
let determineOperation (asmReg: AssemblyRegistration option) (asmLocal) : AssemblyOperation * Guid =
match asmReg with
| Some asm when Compare.registeredIsSameAsLocal asmLocal (Some asm) -> Unchanged, asm.id
| Some asm -> Update, asm.id
| None -> Create, Guid.Empty

/// Update or create assembly
let ensureAssembly proxy solutionName asmLocal maybeAsm =
match Compare.assembly asmLocal maybeAsm with
| true -> maybeAsm.Value.id
| false ->
let asmEntity = EntitySetup.createAssembly asmLocal.dllName asmLocal.dllPath asmLocal.assembly asmLocal.hash asmLocal.isolationMode

match maybeAsm with
| Some asmReg ->
asmEntity.Id <- asmReg.id
match determineOperation maybeAsm asmLocal with
| Unchanged, id ->
log.Info "No changes to assembly %s detected" asmLocal.dllName
id
| Update, id ->
let asmEntity = EntitySetup.createAssembly asmLocal.dllName asmLocal.dllPath asmLocal.assembly asmLocal.hash asmLocal.isolationMode
asmEntity.Id <- id
CrmDataHelper.getResponse<UpdateResponse> proxy (makeUpdateReq asmEntity) |> ignore
log.Info "Updating %s: %s" asmEntity.LogicalName asmLocal.dllName
asmReg.id

| None ->
id
| Create, _ ->
let asmEntity = EntitySetup.createAssembly asmLocal.dllName asmLocal.dllPath asmLocal.assembly asmLocal.hash asmLocal.isolationMode
log.Info "Creating %s: %s" asmEntity.LogicalName asmLocal.dllName
CrmDataHelper.getResponseWithParams<CreateResponse> proxy (makeCreateReq asmEntity) [ "SolutionUniqueName", solutionName ]
|> fun r -> r.id
Expand Down Expand Up @@ -147,10 +154,10 @@ let create proxy solutionName prefix imgDiff stepDiff apiDiff apiReqDiff apiResp


/// Load a local assembly and validate its plugins
let loadAndValidateAssembly proxy projectPath dllPath isolationMode ignoreOutdatedAssembly =
log.Verbose "Loading local assembly and it's plugins"
let asmLocal = PluginDetection.getAssemblyContextFromDll projectPath dllPath isolationMode ignoreOutdatedAssembly
log.Verbose "Local assembly loaded"
let loadAndValidateAssembly proxy dllPath isolationMode =
log.Verbose "Loading local assembly and its plugins"
let asmLocal = PluginDetection.getAssemblyContextFromDll dllPath isolationMode
log.Verbose "Local assembly version %s loaded" (asmLocal.version |> versionToString)

log.Verbose "Validating plugins to be registered"
match Validation.validatePlugins proxy asmLocal.plugins with
Expand All @@ -162,12 +169,12 @@ let loadAndValidateAssembly proxy projectPath dllPath isolationMode ignoreOutdat


/// Analyzes local and remote registrations and returns the information about each of them
let analyze proxyGen projectPath dllPath solutionName isolationMode ignoreOutdatedAssembly =
let analyze proxyGen dllPath solutionName isolationMode =
let proxy = proxyGen()

let asmLocal = loadAndValidateAssembly proxy projectPath dllPath isolationMode ignoreOutdatedAssembly
let asmLocal = loadAndValidateAssembly proxy dllPath isolationMode
let solutionId = CrmDataInternal.Entities.retrieveSolutionId proxy solutionName
let id, prefix = CrmDataInternal.Entities.retrieveSolutionIdAndPrefix proxy solutionName
let _id, prefix = CrmDataInternal.Entities.retrieveSolutionIdAndPrefix proxy solutionName
let asmReg, pluginsReg = Retrieval.retrieveRegisteredByAssembly proxy solutionId asmLocal.dllName
let pluginsLocal = localToMaps asmLocal.plugins asmLocal.customAPIs

Expand Down
67 changes: 5 additions & 62 deletions src/Delegate.Daxif/Modules/Plugins/PluginDetection.fs
Original file line number Diff line number Diff line change
Expand Up @@ -3,59 +3,11 @@
open System
open System.IO
open System.Reflection
open System.Xml.Linq
open DG.Daxif.Common
open DG.Daxif.Common.Utility
open DG.Daxif.Common.InternalUtility

open Domain


/// Used to retrieve a .vsproj dependencies (recursive)
let projDependencies (vsproj:string) =
let getElemName name =
XName.Get(name, "http://schemas.microsoft.com/developer/msbuild/2003")

let getElemValue name (parent : XElement) =
let elem = parent.Element(getElemName name)
if elem = null || String.IsNullOrEmpty elem.Value then None
else Some(elem.Value)

let getAttrValue name (elem : XElement) =
let attr = elem.Attribute(XName.Get name)
if attr = null || String.IsNullOrEmpty attr.Value then None
else Some(attr.Value)

let fullpath path1 path2 = Path.GetFullPath(Path.Combine(path1, path2))

let rec projDependencies' vsproj' = seq {
let vsProjXml = XDocument.Load(uri = vsproj')

let path = Path.GetDirectoryName(vsproj')

let projRefs =
vsProjXml.Document.Descendants(getElemName "ProjectReference")
|> Seq.choose (fun elem -> getAttrValue "Include" elem)
|> Seq.map(fun elem -> fullpath path elem)

let refs =
vsProjXml.Document.Descendants(getElemName "Reference")
|> Seq.choose (fun elem -> getElemValue "HintPath" elem ?|? getAttrValue "Include" elem)
|> Seq.filter (fun ref -> ref.EndsWith(".dll"))
|> Seq.map(fun elem -> fullpath path elem)

let files =
vsProjXml.Document.Descendants(getElemName "Compile")
|> Seq.choose (fun elem -> getAttrValue "Include" elem)
|> Seq.map(fun elem -> fullpath path elem)

for projRef in projRefs do
yield! projDependencies' projRef
yield! refs
yield! files }

projDependencies' (Path.GetFullPath(vsproj))

/// Transforms the received tuple from the assembly file through invocation into
/// plugin, step and image records
let tupleToPlugin
Expand Down Expand Up @@ -260,31 +212,22 @@ let getCustomAPIsFromAssembly (asm: Assembly) =
(getFullException(ex))

/// Analyzes an assembly based on a path to its compiled assembly and its project file
let getAssemblyContextFromDll projectPath dllPath isolationMode ignoreOutdatedAssembly =
let getAssemblyContextFromDll dllPath isolationMode =
let dllFullPath = Path.GetFullPath(dllPath)
let dllTempPath = Path.Combine(Path.GetTempPath(),Guid.NewGuid().ToString() + @".dll")
let dllName = Path.GetFileNameWithoutExtension(dllFullPath);

File.Copy(dllFullPath, dllTempPath, true)

let asmWriteTime = File.GetLastWriteTimeUtc dllFullPath
let hash =
projDependencies projectPath
|> Set.ofSeq
|> Set.map(fun x ->
match not(ignoreOutdatedAssembly) && File.GetLastWriteTimeUtc x > asmWriteTime with
| true -> failwithf "A file in the project was updated later than compiled assembly: %s\nPlease recompile and synchronize again, or use the \"ignoreOutdatedAssembly\" option." x
| false -> File.ReadAllBytes(x) |> sha1CheckSum'
)
|> Set.fold (fun a x -> a + x |> sha1CheckSum) String.Empty

let hash = File.ReadAllBytes dllPath |> sha1CheckSum'
let asm = Assembly.LoadFile(dllTempPath);

let version = asm.GetName().Version |> fun y -> (y.Major, y.Minor, y.Build, y.Revision)

{ assembly = asm
assemblyId = None
dllName = dllName
dllPath = dllFullPath
hash = hash
version = version
isolationMode = isolationMode
plugins = getPluginsFromAssembly asm
customAPIs = getCustomAPIsFromAssembly asm
Expand Down
4 changes: 2 additions & 2 deletions src/Delegate.Daxif/Modules/Plugins/Query.fs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ open Microsoft.Xrm.Sdk.Query
/// Create a query to get a plugin assembly by its name
let pluginAssemblyByName (name: string) =
let q = QueryExpression("pluginassembly")
q.ColumnSet <- ColumnSet("pluginassemblyid", "name", "sourcehash")
q.ColumnSet <- ColumnSet("pluginassemblyid", "name", "sourcehash", "version")

let f = FilterExpression()
f.AddCondition(ConditionExpression("name", ConditionOperator.Equal, name))
Expand All @@ -25,7 +25,7 @@ let pluginAssemblyByName (name: string) =
/// Create a query to get plugin assemblies by solution
let pluginAssembliesBySolution (solutionId: Guid) =
let q = QueryExpression("pluginassembly")
q.ColumnSet <- ColumnSet("pluginassemblyid", "name", "sourcehash", "isolationmode")
q.ColumnSet <- ColumnSet("pluginassemblyid", "name", "sourcehash", "isolationmode", "version")

let le = LinkEntity()
le.JoinOperator <- JoinOperator.Inner
Expand Down
5 changes: 5 additions & 0 deletions src/Delegate.Daxif/Modules/Plugins/Retrieval.fs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ open System
open Microsoft.Xrm.Sdk
open Microsoft.Xrm.Sdk.Messages

open DG.Daxif
open DG.Daxif.Common
open DG.Daxif.Common.Utility

Expand Down Expand Up @@ -98,6 +99,10 @@ let retrieveRegisteredByAssembly proxy solutionId assemblyName =
|> Seq.tryFind (fun a -> getRecordName a = assemblyName)
?|> AssemblyRegistration.fromEntity

match targetAssembly with
| Some asm -> ConsoleLogger.Global.Verbose "Registered assembly version %s found for %s" (asm.version |> versionToString) assemblyName
| None -> ConsoleLogger.Global.Verbose "No registered assembly found matching %s" assemblyName

let maps =
match targetAssembly with
| None -> Map.empty, Map.empty, Map.empty, Map.empty, Map.empty, Map.empty
Expand Down
4 changes: 4 additions & 0 deletions src/Delegate.Daxif/RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
# Release Notes
### 5.6.0 - April 25 2024
* Update assembly comparison when determining if we want to update the assembly. Compare the version of the local assembly to the version currently registered, if the local version is higher (by semver rules) than the registered version, update the assembly even if the hash matches. (@mkholt)
* Changed the hashing functionality to no longer load in the project files and dependencies, but instead taking a SHA1 sum of the assembly file. This removes the dependency on the project files, and makes the hashing more reliable. (@mkholt)

### 5.5.1 - May 23 2023
* Fixed 'useUniqueInstance' parameter to GetCrmServiceClient() with default value: 'false'. - Developer now has to actively enable multiple instances of service client. This is due to potential authentication issues if too many simultaneous tasks are spawning connections (such as WebResourceSync functionality) (@bo-stig-christensen)

Expand Down

0 comments on commit a27098a

Please sign in to comment.