simplify server startup logic by making IObservableServiceClient.Provider into a observable value

This commit is contained in:
Rodrigo B. de Oliveira 2013-03-10 16:25:37 -03:00
Родитель e0e71c4967
Коммит b43876dfc6
14 изменённых файлов: 271 добавлений и 197 удалений

Просмотреть файл

@ -36,7 +36,6 @@ vs {
':CodeEditor.Composition',
':CodeEditor.IO',
':CodeEditor.Testing',
':CodeEditor.Composition.Client',
':CodeEditor.Composition.Server',
':CodeEditor.Reactive',
':CodeEditor.Logging',
@ -125,6 +124,7 @@ ext {
project(':CodeEditor.Logging'),
project(':CodeEditor.Composition'),
project(':CodeEditor.ContentTypes'),
project(':CodeEditor.Reactive'),
project(':CodeEditor.Text.Data'),
project(':CodeEditor.Text.Logic'),
project(':CodeEditor.Text.UI'),

Просмотреть файл

@ -15,12 +15,12 @@ namespace CodeEditor.Composition.Server
{
static void Main()
{
using (var pidFileWriter = new StreamWriter(File.Open(PidFile, FileMode.Create, FileAccess.Write, FileShare.Read)))
using (var uriFileWriter = new StreamWriter(File.Open(UriFilePath, FileMode.Create, FileAccess.Write, FileShare.Read)))
{
var baseUri = "http://localhost:8888/";
const string baseUri = "http://localhost:8888/";
pidFileWriter.Write(baseUri);
pidFileWriter.Flush();
uriFileWriter.WriteLine(baseUri);
uriFileWriter.Flush();
using (var appHost = new AppHost(DirectoryCatalog.AllAssembliesIn(ServerDirectory)))
{
@ -38,9 +38,9 @@ namespace CodeEditor.Composition.Server
get { return Path.GetDirectoryName(FullyQualifiedName); }
}
protected static string PidFile
protected static string UriFilePath
{
get { return Path.ChangeExtension(FullyQualifiedName, "pid"); }
get { return Path.ChangeExtension(FullyQualifiedName, "uri"); }
}
private static string FullyQualifiedName

Просмотреть файл

@ -1,6 +1,5 @@
using System;
using System.Diagnostics;
using System.IO;
using CodeEditor.Composition;
namespace CodeEditor.IO
@ -10,12 +9,11 @@ namespace CodeEditor.IO
IProcess StartManagedProcess(string executable);
}
public interface IProcess
public interface IProcess : IDisposable
{
int Id { get; }
StreamReader StandardOutput { get; }
StreamWriter StandardInput { get; }
bool WaitForExit(int timeout);
void Kill();
}
[Export(typeof(IShell))]
@ -23,15 +21,17 @@ namespace CodeEditor.IO
{
public IProcess StartManagedProcess(string executable)
{
var mono = Environment.GetEnvironmentVariable("MONO_EXECUTABLE") ?? "mono";
return new StandardProcess(Process.Start(new ProcessStartInfo(mono, executable)
return new StandardProcess(Process.Start(new ProcessStartInfo(MonoExecutable, executable)
{
RedirectStandardInput = true,
RedirectStandardOutput = true,
UseShellExecute = false
WindowStyle = ProcessWindowStyle.Minimized
}));
}
static string MonoExecutable
{
get { return Environment.GetEnvironmentVariable("MONO_EXECUTABLE") ?? "mono"; }
}
public class StandardProcess : IProcess
{
private readonly Process _process;
@ -46,20 +46,20 @@ namespace CodeEditor.IO
get { return _process.Id; }
}
public StreamReader StandardOutput
{
get { return _process.StandardOutput; }
}
public StreamWriter StandardInput
{
get { return _process.StandardInput; }
}
public bool WaitForExit(int timeout)
{
return _process.WaitForExit(timeout);
}
public void Kill()
{
_process.Kill();
}
public void Dispose()
{
_process.Dispose();
}
}
}
}

Просмотреть файл

@ -0,0 +1,53 @@
using System;
using System.IO;
using CodeEditor.IO;
using CodeEditor.Logging;
using CodeEditor.Reactive;
using CodeEditor.Testing;
using NUnit.Framework;
namespace CodeEditor.Languages.Common.Tests
{
[TestFixture]
public class ObservableServiceClientProviderTest : MockBasedTest
{
[Test]
public void GetsServerAddressFromUriFile()
{
const string serverExecutable = "server.exe";
const string serverUrilFile = "server.uri";
const string serverAddress = "tcp://localhost:4242/IServiceProvider";
var projectPathProvider = MockFor<IServerExecutableProvider>();
projectPathProvider
.SetupGet(_ => _.ServerExecutable)
.Returns(serverExecutable);
// provider tries to delete pid file to decide if it needs
// to start the server
var fileSystem = MockFor<IFileSystem>();
var uriFile = MockFor<IFile>();
fileSystem
.Setup(_ => _.FileFor(serverUrilFile))
.Returns(uriFile.Object);
uriFile
.Setup(_ => _.Delete())
.Throws(new IOException());
uriFile
.Setup(_ => _.ReadAllText())
.Returns(serverAddress + "\n");
var subject = new ObservableServiceClientProvider
{
ServerExecutableProvider = projectPathProvider.Object,
FileSystem = fileSystem.Object,
Logger = new StandardLogger()
};
Assert.IsNotNull(subject.Client.FirstOrTimeout(TimeSpan.FromSeconds(1)));
VerifyAllMocks();
}
}
}

Просмотреть файл

@ -1,53 +0,0 @@
using System.IO;
using CodeEditor.Composition;
using CodeEditor.IO;
using CodeEditor.Logging;
using CodeEditor.Testing;
using NUnit.Framework;
namespace CodeEditor.Languages.Common.Tests
{
[TestFixture]
public class UnityProjectProviderTest : MockBasedTest
{
[Test]
public void GetsProjectLocationFromLocationProviderAndAddressFromPidFile()
{
const string projectFolder = "/Project";
const string serverAddress = "tcp://localhost:4242/IServiceProvider";
var projectPathProvider = MockFor<IUnityProjectPathProvider>();
projectPathProvider
.SetupGet(_ => _.Location)
.Returns(projectFolder);
var pidFilePath = Path.Combine(projectFolder, "Library/CodeEditor/Server/CodeEditor.Composition.Server.pid");
// provider tries to delete pid file to decide if it needs
// to start the server
var fileSystem = MockFor<IFileSystem>();
var pidFile = MockFor<IFile>();
fileSystem
.Setup(_ => _.FileFor(pidFilePath))
.Returns(pidFile.Object);
pidFile
.Setup(_ => _.Delete())
.Throws(new IOException());
pidFile
.Setup(_ => _.ReadAllText())
.Returns(serverAddress);
var subject = new ObservableServiceClientProvider
{
ProjectPathProvider = projectPathProvider.Object,
FileSystem = fileSystem.Object,
Logger = new StandardLogger()
};
Assert.IsNotNull(subject.Client);
VerifyAllMocks();
}
}
}

Просмотреть файл

@ -1,9 +1,10 @@
using System;
using System.IO;
using CodeEditor.Composition;
using CodeEditor.Composition.Client;
using CodeEditor.IO;
using CodeEditor.Logging;
using CodeEditor.Reactive;
using CodeEditor.Reactive.Disposables;
using CodeEditor.Server.Interface;
using ServiceStack.Text;
using IFile = CodeEditor.IO.IFile;
@ -12,63 +13,74 @@ namespace CodeEditor.Languages.Common
{
public interface IObservableServiceClientProvider
{
IObservableServiceClient Client { get; }
}
public interface IUnityProjectPathProvider
{
string Location { get; }
IObservableX<IObservableServiceClient> Client { get; }
}
[Export(typeof(IObservableServiceClientProvider))]
public class ObservableServiceClientProvider : IObservableServiceClientProvider
{
private readonly Lazy<IObservableServiceClient> _client;
[Import]
public IFileSystem FileSystem;
[Import]
public IUnityProjectPathProvider ProjectPathProvider;
[Import]
public ICompositionServerControllerFactory ControllerFactory;
[Import]
public ILogger Logger;
readonly Lazy<IFile> _serverUriFile;
public ObservableServiceClientProvider()
{
_client = new Lazy<IObservableServiceClient>(CreateClient);
_serverUriFile = new Lazy<IFile>(() => FileSystem.FileFor(ServerUriFilePath));
}
public IObservableServiceClient Client
{
get { return _client.Value; }
}
[Import]
public IServerExecutableProvider ServerExecutableProvider { get; set; }
private IObservableServiceClient CreateClient()
{
try
{
EnsureCompositionServerIsRunning();
[Import]
public IFileSystem FileSystem { get; set; }
var baseUri = PidFile.ReadAllText();
return new ObservableServiceClient(baseUri);
}
catch (Exception e)
[Import]
public IShell Shell { get; set; }
[Import]
public ILogger Logger { get; set; }
public IObservableX<IObservableServiceClient> Client
{
get
{
Logger.LogError(e);
throw;
return
CreateClient()
.Catch(
(Exception e) =>
ObservableX
.Throw<IObservableServiceClient>(e)
.Delay(TimeSpan.FromMilliseconds(500)))
.Retry();
}
}
private IFile PidFile
IObservableX<IObservableServiceClient> CreateClient()
{
get { return FileSystem.FileFor(PidFilePath); }
return ObservableX.CreateWithDisposable<IObservableServiceClient>(observer =>
{
try
{
EnsureCompositionServerIsRunning();
var baseUri = FirstLineFromUriFile();
observer.CompleteWith(new ObservableServiceClient(baseUri));
}
catch (Exception e)
{
Logger.LogError(e);
observer.OnError(e);
}
return Disposable.Empty;
});
}
private void EnsureCompositionServerIsRunning()
string FirstLineFromUriFile()
{
var content = UriFile.ReadAllText();
if (content[content.Length - 1] != '\n')
throw new InvalidOperationException("'{0}' is missing a line ending".Fmt(content));
return content.Trim();
}
void EnsureCompositionServerIsRunning()
{
if (IsRunning())
{
@ -78,44 +90,69 @@ namespace CodeEditor.Languages.Common
StartCompositionContainer();
}
private void StartCompositionContainer()
IFile UriFile
{
var folder = Path.GetDirectoryName(CompositionServerExe);
Logger.Log("Starting server at {0}".Fmt(folder));
ControllerFactory.StartCompositionServerAtFolder(folder);
get { return _serverUriFile.Value; }
}
private bool IsRunning()
void StartCompositionContainer()
{
return !TryToDeleteFilePidFile();
var serverExe = ServerExecutable;
Logger.Log("Starting {0}".Fmt(serverExe));
using (Shell.StartManagedProcess(serverExe))
{
// this doesn't kill the actual process but
// just releases any resources attached to
// the object
}
}
private bool TryToDeleteFilePidFile()
bool IsRunning()
{
return !TryToDeleteUriFile();
}
bool TryToDeleteUriFile()
{
try
{
PidFile.Delete();
UriFile.Delete();
return true;
}
catch (Exception e)
catch (Exception)
{
return false;
}
}
private string PidFilePath
string ServerUriFilePath
{
get { return Path.ChangeExtension(CompositionServerExe, "pid"); }
get { return Path.ChangeExtension(ServerExecutable, "uri"); }
}
private string CompositionServerExe
string ServerExecutable
{
get { return Path.Combine(ProjectFolder, "Library/CodeEditor/Server/CodeEditor.Composition.Server.exe"); }
get { return ServerExecutableProvider.ServerExecutable; }
}
}
public interface IServerExecutableProvider
{
string ServerExecutable { get; }
}
public class ServerExecutableProvider : IServerExecutableProvider
{
readonly string _serverExecutable;
public ServerExecutableProvider(string serverExecutable)
{
_serverExecutable = serverExecutable;
}
protected string ProjectFolder
public string ServerExecutable
{
get { return ProjectPathProvider.Location; }
get { return _serverExecutable; }
}
}
}

Просмотреть файл

@ -7,25 +7,25 @@ using CodeEditor.Text.UI;
namespace CodeEditor.Languages.Common
{
[Export(typeof(INavigateToItemProvider))]
internal class SymbolNavigateToItemProvider : INavigateToItemProvider
public class SymbolNavigateToItemProvider : INavigateToItemProvider
{
[Import]
public IObservableServiceClientProvider ServiceClientProvider;
public IObservableServiceClientProvider ServiceClientProvider { get; set; }
[Import]
public ILogger Logger;
public ILogger Logger { get; set; }
public IObservableX<INavigateToItem> Search(string filter)
{
if (string.IsNullOrEmpty(filter))
return ObservableX.Empty<INavigateToItem>();
return ServiceClient
.ObserveMany(new SymbolSearch {Filter = filter})
.Select(_ => (INavigateToItem)new SymbolItem(_));
return string.IsNullOrEmpty(filter)
? ObservableX.Empty<INavigateToItem>()
: ServiceClient
.SelectMany(
(client) => client.ObserveMany(new SymbolSearch {Filter = filter}),
(client, symbol) => (INavigateToItem) new SymbolItem(symbol));
}
private IObservableServiceClient ServiceClient
private IObservableX<IObservableServiceClient> ServiceClient
{
get { return ServiceClientProvider.Client; }
}

Просмотреть файл

@ -8,5 +8,10 @@ namespace CodeEditor.Reactive.Disposables
{
return System.Disposables.Disposable.Create(dispose);
}
public static IDisposable Empty
{
get { return System.Disposables.Disposable.Empty; }
}
}
}

Просмотреть файл

@ -17,6 +17,15 @@ namespace CodeEditor.Reactive
void OnCompleted();
}
public static class ObserverX
{
public static void CompleteWith<T>(this IObserverX<T> observer, T value)
{
observer.OnNext(value);
observer.OnCompleted();
}
}
public static class ObservableX
{
public static IObservableX<T> ObserveOnThreadPool<T>(this IObservableX<T> source)
@ -39,6 +48,16 @@ namespace CodeEditor.Reactive
return Observable.Start(func).ToObservableX();
}
public static IObservableX<T> Delay<T>(this IObservableX<T> source, TimeSpan dueTimeout)
{
return source.Map(_ => _.Delay(dueTimeout));
}
public static IObservableX<T> Retry<T>(this IObservableX<T> source)
{
return source.Map(_ => _.Retry());
}
public static IObservableX<T> Return<T>(T value)
{
return Observable.Return(value).ToObservableX();
@ -79,6 +98,12 @@ namespace CodeEditor.Reactive
Func<T, IObservable<TResult>> observableSelector = t => selector(t).ToObservable();
return source.Map(_ => _.SelectMany(observableSelector));
}
public static IObservableX<TResult> SelectMany<TSource, TCollection, TResult>(this IObservableX<TSource> source, Func<TSource, IObservableX<TCollection>> collectionSelector, Func<TSource, TCollection, TResult> resultSelector)
{
Func<TSource, IObservable<TCollection>> observableCollectionSelector = t => collectionSelector(t).ToObservable();
return source.Map(_ => _.SelectMany(observableCollectionSelector, resultSelector));
}
public static IObservableX<T> Where<T>(this IObservableX<T> source, Func<T, bool> predicate)
{
@ -90,6 +115,11 @@ namespace CodeEditor.Reactive
return source.Map(_ => _.TakeWhile(predicate));
}
public static IObservableX<T> Take<T>(this IObservableX<T> source, int count)
{
return source.Map(_ => _.Take(count));
}
public static IObservableX<T> Do<T>(this IObservableX<T> source, Action<T> action)
{
return source.Map(_ => _.Do(action));
@ -145,10 +175,20 @@ namespace CodeEditor.Reactive
return Observable.Create<T>(observer => subscribe(observer.ToObserverX())).ToObservableX();
}
public static IObservableX<T> CreateWithDisposable<T>(Func<IObserverX<T>, IDisposable> subscribe)
{
return Observable.CreateWithDisposable<T>(observer => subscribe(observer.ToObserverX())).ToObservableX();
}
public static Func<IObservableX<TResult>> FromAsyncPattern<TResult>(Func<AsyncCallback, object, IAsyncResult> begin, Func<IAsyncResult, TResult> end)
{
var fromAsyncPattern = Observable.FromAsyncPattern(begin, end);
return () => fromAsyncPattern().ToObservableX();
}
public static IObservableX<TResult> Defer<TResult>(Func<IObservableX<TResult>> observableFactory)
{
return Observable.Defer(() => observableFactory().ToObservable()).ToObservableX();
}
}
}

Просмотреть файл

@ -26,7 +26,7 @@ namespace CodeEditor.Server.Interface
public IObservableX<TResponse> ObserveMany<TResponse>(IReturn<IEnumerable<TResponse>> request)
{
return ObservableX.Create<TResponse>(observer =>
return ObservableX.CreateWithDisposable<TResponse>(observer =>
{
var client = new JsonServiceClient(_baseUri) {Timeout = Timeout};
var disposable = new MultipleAssignmentDisposable
@ -47,7 +47,7 @@ namespace CodeEditor.Server.Interface
observer.OnError(exception);
});
return disposable.Dispose;
return disposable;
});
}
}

Просмотреть файл

@ -31,11 +31,6 @@ namespace CodeEditor.Server
IUnityProject Project { get; }
}
public interface IUnityAssetsFolderProvider
{
IFolder AssetsFolder { get; }
}
[Export(typeof(IUnityProjectProvider))]
class UnityProjectProvider : IUnityProjectProvider
{
@ -65,6 +60,11 @@ namespace CodeEditor.Server
}
}
public interface IUnityAssetsFolderProvider
{
IFolder AssetsFolder { get; }
}
[Export(typeof(IUnityAssetsFolderProvider))]
class ServerAssetsFolderProvider : IUnityAssetsFolderProvider
{

Просмотреть файл

@ -1,10 +1,12 @@
using System;
using System.IO;
using System.Linq;
using CodeEditor.Composition;
using CodeEditor.Composition.Hosting;
using CodeEditor.IO;
using CodeEditor.Logging;
using CodeEditor.Text.UI.Unity.Engine;
using CodeEditor.IO;
using CodeEditor.Languages.Common;
using CodeEditor.Logging;
using CodeEditor.Text.UI.Unity.Engine;
using UnityEditor;
namespace CodeEditor.Text.UI.Unity.Editor.Implementation
@ -18,42 +20,54 @@ namespace CodeEditor.Text.UI.Unity.Editor.Implementation
NavigatorWindow.ProviderAggregatorFactory = GetExportedValue<INavigateToItemProviderAggregator>;
}
private static ITextView ViewForFile(string fileName)
static ITextView ViewForFile(string fileName)
{
return GetExportedValue<ITextViewFactory>().ViewForFile(fileName);
}
private static T GetExportedValue<T>()
{
static T GetExportedValue<T>()
{
return CompositionContainer.GetExportedValue<T>();
}
private static CompositionContainer CompositionContainer
static CompositionContainer CompositionContainer
{
get { return Container.Value; }
}
private static readonly Lazy<CompositionContainer> Container = new Lazy<CompositionContainer>(CreateCompositionContainer);
static readonly Lazy<CompositionContainer> Container = new Lazy<CompositionContainer>(CreateCompositionContainer);
private static CompositionContainer CreateCompositionContainer()
static CompositionContainer CreateCompositionContainer()
{
var container = new CompositionContainer(AppDomain.CurrentDomain.GetAssemblies().ToArray());
container.AddExportedValue<IFileSystem>(new UnityEditorFileSystem());
container.AddExportedValue<ILogger>(new UnityLogger());
container.AddExportedValue<IServerExecutableProvider>(new ServerExecutableProvider(ServerExecutable));
if (UnityEngine.Debug.isDebugBuild)
container.AddExportedValue<ILogger>(new UnityLogger());
return container;
}
private class UnityLogger : ILogger
{
public void Log(object value)
{
UnityEngine.Debug.Log(value);
}
public void LogError(Exception exception)
{
UnityEngine.Debug.LogException(exception);
}
}
}
static string ServerExecutable
{
get { return Path.Combine(ProjectPath, "Library/CodeEditor/Server/CodeEditor.Composition.Server.exe"); }
}
static string ProjectPath
{
get { return Path.GetDirectoryName(UnityEngine.Application.dataPath); }
}
class UnityLogger : ILogger
{
public void Log(object value)
{
UnityEngine.Debug.Log(value);
}
public void LogError(Exception exception)
{
UnityEngine.Debug.LogException(exception);
}
}
}
}
}

Просмотреть файл

@ -1,18 +0,0 @@
using System.IO;
using CodeEditor.Composition;
using CodeEditor.Languages.Common;
namespace CodeEditor.Text.UI.Unity.Editor.Implementation
{
[Export(typeof(IUnityProjectPathProvider))]
class UnityProjectPathProvider : IUnityProjectPathProvider
{
public string Location
{
get
{
return Path.GetDirectoryName(UnityEngine.Application.dataPath);
}
}
}
}

Просмотреть файл

@ -1,11 +1,7 @@
* implement IShell.StartManagedProcess on top of Unity's MonoBleedingEdge
* get mono from MONO_EXECUTABLE environment variable in the default case
* kaizen Clr.exec should set MONO_EXECUTABLE
* pass project folder as environment variable to CodeEditor.Composition.Server
* integrate nrefactory for symbol search
* implement streaming behaviour for ObservableServiceClient
* move File search also to the server (using the client search only as a fallback)
* move logging to CodeEditor.Logging
* rename all Implementation folders to Internal
* rename all gradle files to build.gradle
* integrate undo