Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
7361063
initial commit
suhsteve May 13, 2020
cd59f99
add generated files to disposable temp directory
suhsteve May 14, 2020
8e42081
fixes
suhsteve May 15, 2020
bd47b67
clean up
suhsteve May 15, 2020
f7ddf2e
remove enable flag
suhsteve May 15, 2020
3286701
cleanup
suhsteve May 17, 2020
6da419c
serialize metadata
suhsteve May 19, 2020
d7960f9
update csproj
suhsteve May 19, 2020
fcc4c05
Merge branch 'master' into dotnetinteractive_extension
suhsteve May 29, 2020
c3f5d5d
cleanup.
suhsteve May 29, 2020
9d44c4c
set environment variable
suhsteve May 29, 2020
66804ef
only serialize if there is data.
suhsteve May 29, 2020
6abb34c
updates to work in vscode.
suhsteve May 30, 2020
7d1dfae
rename method.
suhsteve May 30, 2020
776b6a5
add comments.
suhsteve May 30, 2020
bb40a8f
update summary.
suhsteve May 30, 2020
864b3d0
use default SparkSession.
suhsteve May 30, 2020
427782b
create temp dir method.
suhsteve May 30, 2020
c4f6321
PR comments.
suhsteve Jun 3, 2020
fb1b76b
add header
suhsteve Jun 3, 2020
9e72edb
remove newline
suhsteve Jun 3, 2020
7e07b75
cleanup.
suhsteve Jun 3, 2020
6100b6a
PR comments.
suhsteve Jun 4, 2020
2259289
emit dll to method.
suhsteve Jun 4, 2020
914a576
cleanup
suhsteve Jun 4, 2020
a73fabb
cleanup
suhsteve Jun 4, 2020
643db70
thread safe
suhsteve Jun 8, 2020
5ecf819
rename PackageHelper.cs
suhsteve Jun 12, 2020
f2e6d0b
rename
suhsteve Jun 12, 2020
e6a1758
PR comments
suhsteve Jun 12, 2020
8d66c25
PR comments.
suhsteve Jun 12, 2020
e75e8eb
remove line breaks.
suhsteve Jun 12, 2020
1ac0e51
commit to force auzre pipeline
suhsteve Jun 13, 2020
828ab62
commit to force auzre pipeline
suhsteve Jun 13, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<RootNamespace>Microsoft.Spark.Extensions.DotNet.Interactive.UnitTest</RootNamespace>
<IsPackable>false</IsPackable>

<RestoreAdditionalProjectSources>https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet3.1/nuget/v3/index.json</RestoreAdditionalProjectSources>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Moq" Version="4.10.0" />
<PackageReference Include="Microsoft.DotNet.Interactive" Version="1.0.0-beta.20262.1" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Microsoft.Spark.Extensions.DotNet.Interactive\Microsoft.Spark.Extensions.DotNet.Interactive.csproj" />
<ProjectReference Include="..\..\Microsoft.Spark\Microsoft.Spark.csproj" />
</ItemGroup>

<ItemGroup>
<Compile Include="..\..\Microsoft.Spark.UnitTest\TestUtils\TemporaryDirectory.cs" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.DotNet.Interactive.Utility;
using Microsoft.Spark.UnitTest.TestUtils;
using Microsoft.Spark.Utils;
using Moq;
using Xunit;

namespace Microsoft.Spark.Extensions.DotNet.Interactive.UnitTest
{
public class PackageResolverTests
{
[Fact]
public void TestPackageResolver()
{
using var tempDir = new TemporaryDirectory();

string packageName = "package.name";
string packageVersion = "0.1.0";
string packageRootPath =
Path.Combine(tempDir.Path, "path", "to", "packages", packageName, packageVersion);
string packageFrameworkPath = Path.Combine(packageRootPath, "lib", "framework");

Directory.CreateDirectory(packageRootPath);
var nugetFile = new FileInfo(
Path.Combine(packageRootPath, $"{packageName}.{packageVersion}.nupkg"));
using (File.Create(nugetFile.FullName))
{
}

var assemblyPaths = new List<FileInfo>
{
new FileInfo(Path.Combine(packageFrameworkPath, "1.dll")),
new FileInfo(Path.Combine(packageFrameworkPath, "2.dll"))
};
var probingPaths = new List<DirectoryInfo> { new DirectoryInfo(packageRootPath) };

var mockPackageRestoreContextWrapper = new Mock<PackageRestoreContextWrapper>();
mockPackageRestoreContextWrapper
.SetupGet(m => m.ResolvedPackageReferences)
.Returns(new ResolvedPackageReference[]
{
new ResolvedPackageReference(
packageName,
packageVersion,
assemblyPaths,
new DirectoryInfo(packageRootPath),
probingPaths)
});

var packageResolver = new PackageResolver(mockPackageRestoreContextWrapper.Object);
IEnumerable<string> actualFiles = packageResolver.GetFiles(tempDir.Path);

string metadataFilePath =
Path.Combine(tempDir.Path, DependencyProviderUtils.CreateFileName(1));
var expectedFiles = new string[]
{
nugetFile.FullName,
metadataFilePath
};
Assert.True(expectedFiles.SequenceEqual(actualFiles));
Assert.True(File.Exists(metadataFilePath));

DependencyProviderUtils.Metadata actualMetadata =
DependencyProviderUtils.Metadata.Deserialize(metadataFilePath);
var expectedMetadata = new DependencyProviderUtils.Metadata
{
AssemblyProbingPaths = new string[]
{
Path.Combine(packageName, packageVersion, "lib", "framework", "1.dll"),
Path.Combine(packageName, packageVersion, "lib", "framework", "2.dll")
},
NativeProbingPaths = new string[]
{
Path.Combine(packageName, packageVersion)
},
NuGets = new DependencyProviderUtils.NuGetMetadata[]
{
new DependencyProviderUtils.NuGetMetadata
{
FileName = $"{packageName}.{packageVersion}.nupkg",
PackageName = packageName,
PackageVersion = packageVersion
}
}
};
Assert.True(expectedMetadata.Equals(actualMetadata));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.DotNet.Interactive;
using Microsoft.DotNet.Interactive.Commands;
using Microsoft.DotNet.Interactive.CSharp;
using Microsoft.DotNet.Interactive.Utility;
using Microsoft.Spark.Interop;
using Microsoft.Spark.Sql;
using Microsoft.Spark.Utils;

namespace Microsoft.Spark.Extensions.DotNet.Interactive
{
/// <summary>
/// A kernel extension when using .NET for Apache Spark with Microsoft.DotNet.Interactive
/// Adds nuget and assembly dependencies to the default <see cref="SparkSession"/>
/// using <see cref="SparkContext.AddFile(string, bool)"/>.
/// </summary>
public class AssemblyKernelExtension : IKernelExtension
{
private const string TempDirEnvVar = "DOTNET_SPARK_EXTENSION_INTERACTIVE_TMPDIR";

private readonly PackageResolver _packageResolver =
new PackageResolver(new PackageRestoreContextWrapper());

/// <summary>
/// Called by the Microsoft.DotNet.Interactive Assembly Extension Loader.
/// </summary>
/// <param name="kernel">The kernel calling this method.</param>
/// <returns><see cref="Task.CompletedTask"/> when extension is loaded.</returns>
public Task OnLoadAsync(IKernel kernel)
{
if (kernel is CompositeKernel kernelBase)
{
Environment.SetEnvironmentVariable(Constants.RunningREPLEnvVar, "true");

DirectoryInfo tempDir = CreateTempDirectory();
kernelBase.RegisterForDisposal(new DisposableDirectory(tempDir));

kernelBase.AddMiddleware(async (command, context, next) =>
{
if ((context.HandlingKernel is CSharpKernel kernel) &&
(command is SubmitCode) &&
TryGetSparkSession(out SparkSession sparkSession) &&
TryEmitAssembly(kernel, tempDir.FullName, out string assemblyPath))
{
sparkSession.SparkContext.AddFile(assemblyPath);

foreach (string filePath in GetPackageFiles(tempDir.FullName))
{
sparkSession.SparkContext.AddFile(filePath);
}
}

await next(command, context);
});
}

return Task.CompletedTask;
}

private DirectoryInfo CreateTempDirectory()
{
string envTempDir = Environment.GetEnvironmentVariable(TempDirEnvVar);
string tempDirBasePath = string.IsNullOrEmpty(envTempDir) ?
Directory.GetCurrentDirectory() :
envTempDir;

if (!IsPathValid(tempDirBasePath))
{
throw new Exception($"[{GetType().Name}] Spaces in " +
$"'{tempDirBasePath}' is unsupported. Set the {TempDirEnvVar} " +
"environment variable to control the base path. Please see " +
"https://issues.apache.org/jira/browse/SPARK-30126 and " +
"https://github.com/apache/spark/pull/26773 for more details.");
}

return Directory.CreateDirectory(
Path.Combine(tempDirBasePath, Path.GetRandomFileName()));
}

private bool TryEmitAssembly(CSharpKernel kernel, string dstPath, out string assemblyPath)
{
Compilation compilation = kernel.ScriptState.Script.GetCompilation();
string assemblyName =
AssemblyLoader.NormalizeAssemblyName(compilation.AssemblyName);
assemblyPath = Path.Combine(dstPath, $"{assemblyName}.dll");
if (!File.Exists(assemblyPath))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought this was a critical error scenario no?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it's expected. Should we log an error or throw an exception ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's throw an exception to fail fast

{
FileSystemExtensions.Emit(compilation, assemblyPath);
return true;
}

throw new Exception(
$"TryEmitAssembly() unexpected duplicate assembly: ${assemblyPath}");
}

private bool TryGetSparkSession(out SparkSession sparkSession)
{
sparkSession = SparkSession.GetDefaultSession();
return sparkSession != null;
}

private IEnumerable<string> GetPackageFiles(string path)
{
foreach (string filePath in _packageResolver.GetFiles(path))
{
if (IsPathValid(filePath))
{
yield return filePath;
}
else
{
// Copy file to a path without spaces.
string fileDestPath = Path.Combine(
path,
Path.GetFileName(filePath).Replace(" ", string.Empty));
File.Copy(filePath, fileDestPath);
yield return fileDestPath;
}
}
}

/// <summary>
/// In some versions of Spark, spaces is unsupported when using
/// <see cref="SparkContext.AddFile(string, bool)"/>.
///
/// For more details please see:
/// - https://issues.apache.org/jira/browse/SPARK-30126
/// - https://github.com/apache/spark/pull/26773
/// </summary>
/// <param name="path">The path to validate.</param>
/// <returns>true if the path is supported by Spark, false otherwise.</returns>
private bool IsPathValid(string path)
{
if (!path.Contains(" "))
{
return true;
}

Version version = SparkEnvironment.SparkVersion;
return (version.Major, version.Minor, version.Build) switch
{
(2, _, _) => false,
(3, 0, _) => true,
_ => throw new NotSupportedException($"Spark {version} not supported.")
};
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<RootNamespace>Microsoft.Spark.Extensions.DotNet.Interactive</RootNamespace>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<IsPackable>true</IsPackable>
<!-- NU5100 warns that a dll was found outside the 'lib' folder while packaging. DotNet.Interactive expects extension dlls in the 'interactive-extensions/dotnet'. -->
<NoWarn>NU5100;$(NoWarn)</NoWarn>

<Description>DotNet Interactive Extension for .NET for Apache Spark</Description>
<PackageReleaseNotes>https://github.com/dotnet/spark/tree/master/docs/release-notes</PackageReleaseNotes>
<PackageTags>spark;dotnet;csharp;interactive;dotnet-interactive</PackageTags>

<RestoreAdditionalProjectSources>https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet3.1/nuget/v3/index.json</RestoreAdditionalProjectSources>
</PropertyGroup>

<ItemGroup>
<InternalsVisibleTo Include="Microsoft.Spark.Extensions.DotNet.Interactive.UnitTest" />
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" Key="0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.DotNet.Interactive.CSharp" Version="1.0.0-beta.20262.1">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\Microsoft.Spark\Microsoft.Spark.csproj" />
</ItemGroup>

<ItemGroup>
<None Include="$(OutputPath)/Microsoft.Spark.Extensions.DotNet.Interactive.dll"
Pack="true"
PackagePath="interactive-extensions/dotnet" />
</ItemGroup>

</Project>
Loading