Skip to content

Commit a45b452

Browse files
committed
Shell for test FTP server can now list and close connections
Copied ReadLine into third-party and changed to allow async auto completion handlers.
1 parent 318fb3c commit a45b452

26 files changed

+926
-36
lines changed

FubarDev.FtpServer.sln

+7-3
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
99
ProjectSection(SolutionItems) = preProject
1010
.dockerignore = .dockerignore
1111
.editorconfig = .editorconfig
12+
azure-pipelines.yml = azure-pipelines.yml
1213
FubarDev.FtpServer.ruleset = FubarDev.FtpServer.ruleset
1314
Global.props = Global.props
1415
LICENSE.md = LICENSE.md
1516
PackageLibrary.props = PackageLibrary.props
1617
README.md = README.md
1718
stylecop.json = stylecop.json
18-
azure-pipelines.yml = azure-pipelines.yml
1919
EndProjectSection
2020
EndProject
2121
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FubarDev.FtpServer.FileSystem.DotNet", "src\FubarDev.FtpServer.FileSystem.DotNet\FubarDev.FtpServer.FileSystem.DotNet.csproj", "{9A83C6F5-378B-48B3-B32A-151DB90B390C}"
@@ -60,14 +60,17 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestFtpServer.Shell", "samp
6060
EndProject
6161
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestFtpServer.Api", "samples\TestFtpServer.Api\TestFtpServer.Api.csproj", "{02D103B2-B9CA-4A7F-AB79-6FEC68C62B47}"
6262
EndProject
63-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuickStart.GenericHost", "samples\QuickStart.GenericHost\QuickStart.GenericHost.csproj", "{23752176-0F1A-4352-B677-CD7878B7E8FC}"
63+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QuickStart.GenericHost", "samples\QuickStart.GenericHost\QuickStart.GenericHost.csproj", "{23752176-0F1A-4352-B677-CD7878B7E8FC}"
64+
EndProject
65+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QuickStart.AspNetCoreHost", "samples\QuickStart.AspNetCoreHost\QuickStart.AspNetCoreHost.csproj", "{D03594D1-84CC-4B55-A56D-20ED446CFDB1}"
6466
EndProject
65-
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "QuickStart.AspNetCoreHost", "samples\QuickStart.AspNetCoreHost\QuickStart.AspNetCoreHost.csproj", "{D03594D1-84CC-4B55-A56D-20ED446CFDB1}"
67+
Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "ReadLine", "third-party\ReadLine\ReadLine.shproj", "{F68F448F-E8A4-4536-9A83-12018B6294B0}"
6668
EndProject
6769
Global
6870
GlobalSection(SharedMSBuildProjectFiles) = preSolution
6971
src\FubarDev.FtpServer.Shared\FubarDev.FtpServer.Shared.projitems*{52126b62-de83-4597-a832-d144f9a5a870}*SharedItemsImports = 13
7072
third-party\DotNet.Glob\DotNet.Glob.projitems*{a45290b4-20f7-48e9-8543-c69240bfb9f8}*SharedItemsImports = 13
73+
third-party\ReadLine\ReadLine.projitems*{f68f448f-e8a4-4536-9a83-12018b6294b0}*SharedItemsImports = 13
7174
third-party\GnuSslStream\GnuSslStream.projitems*{fd524518-e097-4ac0-aac8-d5ea24f31545}*SharedItemsImports = 13
7275
EndGlobalSection
7376
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -160,6 +163,7 @@ Global
160163
{02D103B2-B9CA-4A7F-AB79-6FEC68C62B47} = {7004709E-5C0D-4D3E-AF27-89968FA47C19}
161164
{23752176-0F1A-4352-B677-CD7878B7E8FC} = {7004709E-5C0D-4D3E-AF27-89968FA47C19}
162165
{D03594D1-84CC-4B55-A56D-20ED446CFDB1} = {7004709E-5C0D-4D3E-AF27-89968FA47C19}
166+
{F68F448F-E8A4-4536-9A83-12018B6294B0} = {441F13FB-D457-402E-8DAC-4B6485AFCE30}
163167
EndGlobalSection
164168
GlobalSection(ExtensibilityGlobals) = postSolution
165169
SolutionGuid = {DAA7D00E-C61D-4640-A54E-D307E4682263}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// <copyright file="FtpConnectionStatus.cs" company="Fubar Development Junker">
2+
// Copyright (c) Fubar Development Junker. All rights reserved.
3+
// </copyright>
4+
5+
namespace TestFtpServer.Api
6+
{
7+
/// <summary>
8+
/// Information about a single FTP connection.
9+
/// </summary>
10+
public class FtpConnectionStatus
11+
{
12+
/// <summary>
13+
/// Initializes a new instance of the <see cref="FtpConnectionStatus"/> class.
14+
/// </summary>
15+
/// <param name="id">The ID of the connection.</param>
16+
public FtpConnectionStatus(string id)
17+
{
18+
Id = id;
19+
}
20+
21+
/// <summary>
22+
/// Gets or sets the ID of the connection.
23+
/// </summary>
24+
public string Id { get; set; }
25+
26+
/// <summary>
27+
/// Gets or sets a value indicating whether the connection is alive (read: not expired).
28+
/// </summary>
29+
public bool IsAlive { get; set; }
30+
}
31+
}

samples/TestFtpServer.Api/IFtpServerHost.cs

+13
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,19 @@ public interface IFtpServerHost
3030
/// <returns>The task.</returns>
3131
Task StopAsync();
3232

33+
/// <summary>
34+
/// Gets all active connections.
35+
/// </summary>
36+
/// <returns></returns>
37+
ICollection<FtpConnectionStatus> GetConnections();
38+
39+
/// <summary>
40+
/// Closes the connection.
41+
/// </summary>
42+
/// <param name="connectionId">The ID of the connection to be closed.</param>
43+
/// <returns>The task.</returns>
44+
Task CloseConnectionAsync(string connectionId);
45+
3346
/// <summary>
3447
/// Get the list of registered simple modules.
3548
/// </summary>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// <copyright file="CloseConnectionCommandHandler.cs" company="Fubar Development Junker">
2+
// Copyright (c) Fubar Development Junker. All rights reserved.
3+
// </copyright>
4+
5+
using System;
6+
using System.Collections.Generic;
7+
using System.Linq;
8+
using System.Runtime.CompilerServices;
9+
using System.Threading;
10+
using System.Threading.Tasks;
11+
12+
using JKang.IpcServiceFramework;
13+
14+
using TestFtpServer.Api;
15+
16+
namespace TestFtpServer.Shell.Commands
17+
{
18+
/// <summary>
19+
/// Command handler for closing a client FTP connection.
20+
/// </summary>
21+
public class CloseConnectionCommandHandler : ICommandInfo
22+
{
23+
private readonly IpcServiceClient<IFtpServerHost> _client;
24+
25+
/// <summary>
26+
/// Initializes a new instance of the <see cref="CloseConnectionCommandHandler"/> class.
27+
/// </summary>
28+
/// <param name="client">The IPC client.</param>
29+
public CloseConnectionCommandHandler(IpcServiceClient<IFtpServerHost> client)
30+
{
31+
_client = client;
32+
}
33+
34+
/// <inheritdoc />
35+
public string Name { get; } = "connection";
36+
37+
/// <inheritdoc />
38+
public IReadOnlyCollection<string> AlternativeNames { get; } = Array.Empty<string>();
39+
40+
/// <inheritdoc />
41+
public async IAsyncEnumerable<ICommandInfo> GetSubCommandsAsync(
42+
[EnumeratorCancellation] CancellationToken cancellationToken)
43+
{
44+
var connections = await _client
45+
.InvokeAsync(host => host.GetConnections(), cancellationToken)
46+
.ConfigureAwait(false);
47+
foreach (var connection in connections)
48+
{
49+
yield return new CloseConnectionFinalCommandHandler(_client, connection.Id);
50+
}
51+
}
52+
53+
private class CloseConnectionFinalCommandHandler : IExecutableCommandInfo
54+
{
55+
private readonly IpcServiceClient<IFtpServerHost> _client;
56+
57+
/// <summary>
58+
/// Initializes a new instance of the <see cref="CloseConnectionFinalCommandHandler"/> class.
59+
/// </summary>
60+
/// <param name="client">The IPC client.</param>
61+
/// <param name="connectionId">The FTP connection ID.</param>
62+
public CloseConnectionFinalCommandHandler(
63+
IpcServiceClient<IFtpServerHost> client,
64+
string connectionId)
65+
{
66+
_client = client;
67+
Name = connectionId;
68+
}
69+
70+
/// <inheritdoc />
71+
public string Name { get; }
72+
73+
/// <inheritdoc />
74+
public IReadOnlyCollection<string> AlternativeNames { get; } = Array.Empty<string>();
75+
76+
/// <inheritdoc />
77+
public IAsyncEnumerable<ICommandInfo> GetSubCommandsAsync(CancellationToken cancellationToken)
78+
=> AsyncEnumerable.Empty<ICommandInfo>();
79+
80+
/// <inheritdoc />
81+
public Task ExecuteAsync(CancellationToken cancellationToken)
82+
{
83+
return _client.InvokeAsync(host => host.CloseConnectionAsync(Name), cancellationToken);
84+
}
85+
}
86+
}
87+
}

samples/TestFtpServer.Shell/Commands/ContinueCommandHandler.cs

+4-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
using System;
66
using System.Collections.Generic;
7+
using System.Linq;
78
using System.Threading;
89
using System.Threading.Tasks;
910

@@ -33,8 +34,10 @@ public ContinueCommandHandler(IpcServiceClient<Api.IFtpServerHost> client)
3334
/// <inheritdoc />
3435
public IReadOnlyCollection<string> AlternativeNames { get; } = new[] { "resume" };
3536

37+
/// <param name="cancellationToken"></param>
3638
/// <inheritdoc />
37-
public IReadOnlyCollection<ICommandInfo> SubCommands { get; } = Array.Empty<ICommandInfo>();
39+
public IAsyncEnumerable<ICommandInfo> GetSubCommandsAsync(CancellationToken cancellationToken)
40+
=> AsyncEnumerable.Empty<ICommandInfo>();
3841

3942
/// <inheritdoc />
4043
public Task ExecuteAsync(CancellationToken cancellationToken)

samples/TestFtpServer.Shell/Commands/ExitCommandHandler.cs

+19-4
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@
22
// Copyright (c) Fubar Development Junker. All rights reserved.
33
// </copyright>
44

5-
using System;
65
using System.Collections.Generic;
6+
using System.Linq;
77
using System.Threading;
88
using System.Threading.Tasks;
99

10+
using JKang.IpcServiceFramework;
11+
12+
using TestFtpServer.Api;
13+
1014
namespace TestFtpServer.Shell.Commands
1115
{
1216
/// <summary>
@@ -15,24 +19,35 @@ namespace TestFtpServer.Shell.Commands
1519
public class ExitCommandHandler : IRootCommandInfo, IExecutableCommandInfo
1620
{
1721
private readonly IShellStatus _status;
22+
private readonly IAsyncEnumerable<ICommandInfo> _subCommands;
1823

1924
/// <summary>
2025
/// Initializes a new instance of the <see cref="IExecutableCommandInfo"/> class.
2126
/// </summary>
27+
/// <param name="client">The IPC client.</param>
2228
/// <param name="status">The shell status.</param>
23-
public ExitCommandHandler(IShellStatus status)
29+
public ExitCommandHandler(
30+
IpcServiceClient<IFtpServerHost> client,
31+
IShellStatus status)
2432
{
2533
_status = status;
34+
_subCommands = new ICommandInfo[]
35+
{
36+
new CloseConnectionCommandHandler(client),
37+
}
38+
.ToAsyncEnumerable();
2639
}
2740

2841
/// <inheritdoc />
2942
public string Name { get; } = "exit";
3043

3144
/// <inheritdoc />
32-
public IReadOnlyCollection<string> AlternativeNames { get; } = new[] { "quit" };
45+
public IReadOnlyCollection<string> AlternativeNames { get; } = new[] { "quit", "close" };
3346

47+
/// <param name="cancellationToken"></param>
3448
/// <inheritdoc />
35-
public IReadOnlyCollection<ICommandInfo> SubCommands { get; } = Array.Empty<ICommandInfo>();
49+
public IAsyncEnumerable<ICommandInfo> GetSubCommandsAsync(CancellationToken cancellationToken)
50+
=> _subCommands;
3651

3752
/// <inheritdoc />
3853
public Task ExecuteAsync(CancellationToken cancellationToken)

samples/TestFtpServer.Shell/Commands/HelpCommandHandler.cs

+4-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
using System;
66
using System.Collections.Generic;
7+
using System.Linq;
78
using System.Threading;
89
using System.Threading.Tasks;
910

@@ -28,8 +29,10 @@ public HelpCommandHandler(
2829
/// <inheritdoc />
2930
public IReadOnlyCollection<string> AlternativeNames { get; } = Array.Empty<string>();
3031

32+
/// <param name="cancellationToken"></param>
3133
/// <inheritdoc />
32-
public IReadOnlyCollection<ICommandInfo> SubCommands { get; } = Array.Empty<ICommandInfo>();
34+
public IAsyncEnumerable<ICommandInfo> GetSubCommandsAsync(CancellationToken cancellationToken)
35+
=> AsyncEnumerable.Empty<ICommandInfo>();
3336

3437
/// <inheritdoc />
3538
public Task ExecuteAsync(CancellationToken cancellationToken)

samples/TestFtpServer.Shell/Commands/PauseCommandHandler.cs

+4-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
using System;
66
using System.Collections.Generic;
7+
using System.Linq;
78
using System.Threading;
89
using System.Threading.Tasks;
910

@@ -35,8 +36,10 @@ public PauseCommandHandler(IpcServiceClient<IFtpServerHost> client)
3536
/// <inheritdoc />
3637
public IReadOnlyCollection<string> AlternativeNames { get; } = Array.Empty<string>();
3738

39+
/// <param name="cancellationToken"></param>
3840
/// <inheritdoc />
39-
public IReadOnlyCollection<ICommandInfo> SubCommands { get; } = Array.Empty<ICommandInfo>();
41+
public IAsyncEnumerable<ICommandInfo> GetSubCommandsAsync(CancellationToken cancellationToken)
42+
=> AsyncEnumerable.Empty<ICommandInfo>();
4043

4144
/// <inheritdoc />
4245
public Task ExecuteAsync(CancellationToken cancellationToken)

samples/TestFtpServer.Shell/Commands/ShowCommandHandler.cs

+16-5
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ namespace TestFtpServer.Shell.Commands
1919
/// </summary>
2020
public class ShowCommandHandler : IRootCommandInfo
2121
{
22+
private readonly IAsyncEnumerable<ICommandInfo> _subCommands;
23+
2224
/// <summary>
2325
/// Initializes a new instance of the <see cref="ShowCommandHandler"/> class.
2426
/// </summary>
@@ -28,19 +30,26 @@ public ShowCommandHandler(
2830
IpcServiceClient<IFtpServerHost> client,
2931
IShellStatus status)
3032
{
31-
SubCommands = status.ExtendedModuleInfoName
33+
_subCommands = status.ExtendedModuleInfoName
3234
.Select(x => new ModuleCommandInfo(client, x))
33-
.ToList();
35+
.Concat(
36+
new ICommandInfo[]
37+
{
38+
new ShowConnectionsCommandInfo(client),
39+
})
40+
.ToList()
41+
.ToAsyncEnumerable();
3442
}
3543

3644
/// <inheritdoc />
3745
public string Name { get; } = "show";
3846

3947
/// <inheritdoc />
40-
public IReadOnlyCollection<string> AlternativeNames { get; } = Array.Empty<string>();
48+
public IReadOnlyCollection<string> AlternativeNames { get; } = new[] { "list" };
4149

50+
/// <param name="cancellationToken"></param>
4251
/// <inheritdoc />
43-
public IReadOnlyCollection<ICommandInfo> SubCommands { get; }
52+
public IAsyncEnumerable<ICommandInfo> GetSubCommandsAsync(CancellationToken cancellationToken) => _subCommands;
4453

4554
private class ModuleCommandInfo : IExecutableCommandInfo
4655
{
@@ -65,8 +74,10 @@ public ModuleCommandInfo(
6574
/// <inheritdoc />
6675
public IReadOnlyCollection<string> AlternativeNames { get; } = Array.Empty<string>();
6776

77+
/// <param name="cancellationToken"></param>
6878
/// <inheritdoc />
69-
public IReadOnlyCollection<ICommandInfo> SubCommands { get; } = Array.Empty<ICommandInfo>();
79+
public IAsyncEnumerable<ICommandInfo> GetSubCommandsAsync(CancellationToken cancellationToken)
80+
=> AsyncEnumerable.Empty<ICommandInfo>();
7081

7182
/// <inheritdoc />
7283
public async Task ExecuteAsync(CancellationToken cancellationToken)

0 commit comments

Comments
 (0)