提交 3097e623 编写于 作者: J Jason Malinowski

Merge pull request #11440 from CyrusNajmabadi/delayLoadNuget

Delay loading Nuget dlls until we actually need them.
......@@ -91,8 +91,8 @@ protected override void LoadComponentsInUIContext()
_packageInstallerService = Workspace.Services.GetService<IPackageInstallerService>() as PackageInstallerService;
_symbolSearchService = Workspace.Services.GetService<ISymbolSearchService>() as SymbolSearchService;
_packageInstallerService?.Start();
_symbolSearchService?.Start(this.RoslynLanguageName);
_packageInstallerService?.Connect(this.RoslynLanguageName);
_symbolSearchService?.Connect(this.RoslynLanguageName);
}
protected abstract VisualStudioWorkspaceImpl CreateWorkspace();
......@@ -125,8 +125,8 @@ protected void RegisterLanguageService(Type t, Func<object> serviceCreator)
protected override void Dispose(bool disposing)
{
_packageInstallerService?.Stop();
_symbolSearchService?.Stop(this.RoslynLanguageName);
_packageInstallerService?.Disconnect(this.RoslynLanguageName);
_symbolSearchService?.Disconnect(this.RoslynLanguageName);
if (_miscellaneousFilesWorkspace != null)
{
......
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using EnvDTE;
using NuGet.VisualStudio;
namespace Microsoft.VisualStudio.LanguageServices.Packaging
{
// Wrapper types to ensure we delay load the nuget libraries.
internal interface IPackageServicesProxy
{
event EventHandler SourcesChanged;
/// <summary>
/// This method just forwards along <see cref="IVsPackageSourceProvider.GetSources(bool, bool)"/>
/// </summary>
IEnumerable<KeyValuePair<string, string>> GetSources(bool includeUnOfficial, bool includeDisabled);
IEnumerable<PackageMetadata> GetInstalledPackages(Project project);
bool IsPackageInstalled(Project project, string id);
void InstallPackage(string source, Project project, string packageId, string version, bool ignoreDependencies);
void UninstallPackage(Project project, string packageId, bool removeDependencies);
}
internal class PackageMetadata
{
public readonly string Id;
public readonly string VersionString;
public PackageMetadata(string id, string versionString)
{
Id = id;
VersionString = versionString;
}
}
}
......@@ -5,15 +5,11 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Composition;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Notification;
using Microsoft.CodeAnalysis.Packaging;
......@@ -23,6 +19,7 @@
using Microsoft.VisualStudio.ComponentModelHost;
using Microsoft.VisualStudio.Editor;
using Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem;
using Microsoft.VisualStudio.LanguageServices.SymbolSearch;
using Microsoft.VisualStudio.Shell.Interop;
using NuGet.VisualStudio;
using Roslyn.Utilities;
......@@ -39,16 +36,15 @@ namespace Microsoft.VisualStudio.LanguageServices.Packaging
/// the data so it can be read from the background.
/// </summary>
[ExportWorkspaceService(typeof(IPackageInstallerService)), Shared]
internal partial class PackageInstallerService : ForegroundThreadAffinitizedObject, IPackageInstallerService, IVsSearchProviderCallback
internal partial class PackageInstallerService : AbstractDelayStartedService, IPackageInstallerService, IVsSearchProviderCallback
{
private readonly object _gate = new object();
private readonly VisualStudioWorkspaceImpl _workspace;
private readonly IVsEditorAdaptersFactoryService _editorAdaptersFactoryService;
private IVsPackageInstallerServices _packageInstallerServices;
private IVsPackageInstaller _packageInstaller;
private IVsPackageUninstaller _packageUninstaller;
private IVsPackageSourceProvider _packageSourceProvider;
// We refer to the package services through proxy types so that we can
// delay loading their DLLs until we actually need them.
private IPackageServicesProxy _packageServices;
private CancellationTokenSource _tokenSource = new CancellationTokenSource();
......@@ -65,6 +61,9 @@ internal partial class PackageInstallerService : ForegroundThreadAffinitizedObje
public PackageInstallerService(
VisualStudioWorkspaceImpl workspace,
IVsEditorAdaptersFactoryService editorAdaptersFactoryService)
: base(workspace, ServiceComponentOnOffOptions.SymbolSearch,
AddImportOptions.SuggestForTypesInReferenceAssemblies,
AddImportOptions.SuggestForTypesInNuGetPackages)
{
_workspace = workspace;
_editorAdaptersFactoryService = editorAdaptersFactoryService;
......@@ -74,49 +73,49 @@ internal partial class PackageInstallerService : ForegroundThreadAffinitizedObje
public event EventHandler PackageSourcesChanged;
internal void Start()
public bool IsEnabled => _packageServices != null;
protected override void EnableService()
{
this.AssertIsForeground();
// Our service has been enabled. Now load the VS package dlls.
var componentModel = _workspace.GetVsService<SComponentModel, IComponentModel>();
var options = _workspace.Options;
if (!options.GetOption(ServiceComponentOnOffOptions.SymbolSearch))
var packageInstallerServices = componentModel.GetExtensions<IVsPackageInstallerServices>().FirstOrDefault();
var packageInstaller = componentModel.GetExtensions<IVsPackageInstaller>().FirstOrDefault();
var packageUninstaller = componentModel.GetExtensions<IVsPackageUninstaller>().FirstOrDefault();
var packageSourceProvider = componentModel.GetExtensions<IVsPackageSourceProvider>().FirstOrDefault();
if (packageInstallerServices == null ||
packageInstaller == null ||
packageUninstaller == null ||
packageSourceProvider == null)
{
return;
}
StartWorker();
_packageServices = new PackageServicesProxy(
packageInstallerServices, packageInstaller, packageUninstaller, packageSourceProvider);
// Start listening to additional events workspace changes.
_workspace.WorkspaceChanged += OnWorkspaceChanged;
_packageServices.SourcesChanged += OnSourceProviderSourcesChanged;
}
// Don't inline this method. The references to nuget types will cause the nuget packages
// to load.
[MethodImpl(MethodImplOptions.NoInlining)]
private void StartWorker()
protected override void StopWorking()
{
var componentModel = _workspace.GetVsService<SComponentModel, IComponentModel>();
_packageInstallerServices = componentModel.GetExtensions<IVsPackageInstallerServices>().FirstOrDefault();
_packageInstaller = componentModel.GetExtensions<IVsPackageInstaller>().FirstOrDefault();
_packageUninstaller = componentModel.GetExtensions<IVsPackageUninstaller>().FirstOrDefault();
_packageSourceProvider = componentModel.GetExtensions<IVsPackageSourceProvider>().FirstOrDefault();
this.AssertIsForeground();
if (!this.IsEnabled)
if (!IsEnabled)
{
return;
}
// Start listening to workspace changes.
_workspace.WorkspaceChanged += OnWorkspaceChanged;
_packageSourceProvider.SourcesChanged += OnSourceProviderSourcesChanged;
OnSourceProviderSourcesChanged(null, EventArgs.Empty);
// Stop listening to workspace changes.
_workspace.WorkspaceChanged -= OnWorkspaceChanged;
_packageServices.SourcesChanged -= OnSourceProviderSourcesChanged;
}
public bool IsEnabled =>
_packageInstallerServices != null &&
_packageInstallerServices != null &&
_packageUninstaller != null &&
_packageSourceProvider != null;
internal void Stop()
protected override void StartWorking()
{
this.AssertIsForeground();
......@@ -125,8 +124,7 @@ internal void Stop()
return;
}
_packageSourceProvider.SourcesChanged -= OnSourceProviderSourcesChanged;
_workspace.WorkspaceChanged -= OnWorkspaceChanged;
OnSourceProviderSourcesChanged(this, EventArgs.Empty);
}
private void OnSourceProviderSourcesChanged(object sender, EventArgs e)
......@@ -139,7 +137,7 @@ private void OnSourceProviderSourcesChanged(object sender, EventArgs e)
this.AssertIsForeground();
PackageSources = _packageSourceProvider.GetSources(includeUnOfficial: true, includeDisabled: false)
PackageSources = _packageServices.GetSources(includeUnOfficial: true, includeDisabled: false)
.Select(r => new PackageSource(r.Key, r.Value))
.ToImmutableArrayOrEmpty();
......@@ -159,7 +157,7 @@ private void OnSourceProviderSourcesChanged(object sender, EventArgs e)
// The 'workspace == _workspace' line is probably not necessary. However, we include
// it just to make sure that someone isn't trying to install a package into a workspace
// other than the VisualStudioWorkspace.
if (workspace == _workspace && _workspace != null && _packageInstallerServices != null)
if (workspace == _workspace && _workspace != null && _packageServices != null)
{
var projectId = documentId.ProjectId;
var dte = _workspace.GetVsService<SDTE, EnvDTE.DTE>();
......@@ -190,10 +188,10 @@ private void OnSourceProviderSourcesChanged(object sender, EventArgs e)
{
try
{
if (!_packageInstallerServices.IsPackageInstalled(dteProject, packageName))
if (!_packageServices.IsPackageInstalled(dteProject, packageName))
{
dte.StatusBar.Text = string.Format(ServicesVSResources.Installing_0, packageName);
_packageInstaller.InstallPackage(source, dteProject, packageName, versionOpt, ignoreDependencies: false);
_packageServices.InstallPackage(source, dteProject, packageName, versionOpt, ignoreDependencies: false);
var installedVersion = GetInstalledVersion(packageName, dteProject);
dte.StatusBar.Text = string.Format(ServicesVSResources.Installing_0_completed,
......@@ -231,13 +229,13 @@ private static string GetStatusBarText(string packageName, string installedVersi
try
{
if (_packageInstallerServices.IsPackageInstalled(dteProject, packageName))
if (_packageServices.IsPackageInstalled(dteProject, packageName))
{
dte.StatusBar.Text = string.Format(ServicesVSResources.Uninstalling_0, packageName);
var installedVersion = GetInstalledVersion(packageName, dteProject);
_packageUninstaller.UninstallPackage(dteProject, packageName, removeDependencies: true);
_packageServices.UninstallPackage(dteProject, packageName, removeDependencies: true);
dte.StatusBar.Text = string.Format(ServicesVSResources.Uninstalling_0_completed,
dte.StatusBar.Text = string.Format(ServicesVSResources.Uninstalling_0_completed,
GetStatusBarText(packageName, installedVersion));
return true;
......@@ -266,7 +264,7 @@ private string GetInstalledVersion(string packageName, EnvDTE.Project dteProject
try
{
var installedPackages = _packageInstallerServices.GetInstalledPackages(dteProject);
var installedPackages = _packageServices.GetInstalledPackages(dteProject);
var metadata = installedPackages.FirstOrDefault(m => m.Id == packageName);
return metadata?.VersionString;
}
......@@ -337,7 +335,7 @@ private void ProcessBatchedChangesOnForeground(CancellationToken cancellationTok
}
// If we've been disconnected, then there's no point proceeding.
if (_workspace == null || _packageInstallerServices == null)
if (_workspace == null || _packageServices == null)
{
return;
}
......@@ -412,7 +410,7 @@ private void ProcessProjectChange(Solution solution, ProjectId projectId)
// Calling into nuget. Assume they may fail for any reason.
try
{
var installedPackageMetadata = _packageInstallerServices.GetInstalledPackages(dteProject);
var installedPackageMetadata = _packageServices.GetInstalledPackages(dteProject);
installedPackages.AddRange(installedPackageMetadata.Select(m => new KeyValuePair<string, string>(m.Id, m.VersionString)));
}
catch (Exception e) when (FatalError.ReportWithoutCrash(e))
......@@ -570,5 +568,61 @@ public uint GetTokens(uint dwMaxTokens, IVsSearchToken[] rgpSearchTokens)
return 0;
}
}
private class PackageServicesProxy : IPackageServicesProxy
{
private readonly IVsPackageInstaller _packageInstaller;
private readonly IVsPackageInstallerServices _packageInstallerServices;
private readonly IVsPackageSourceProvider _packageSourceProvider;
private readonly IVsPackageUninstaller _packageUninstaller;
public PackageServicesProxy(IVsPackageInstallerServices packageInstallerServices, IVsPackageInstaller packageInstaller, IVsPackageUninstaller packageUninstaller, IVsPackageSourceProvider packageSourceProvider)
{
_packageInstallerServices = packageInstallerServices;
_packageInstaller = packageInstaller;
_packageUninstaller = packageUninstaller;
_packageSourceProvider = packageSourceProvider;
}
public IEnumerable<PackageMetadata> GetInstalledPackages(EnvDTE.Project project)
{
return _packageInstallerServices.GetInstalledPackages(project)
.Select(m => new PackageMetadata(m.Id, m.VersionString))
.ToList();
}
public bool IsPackageInstalled(EnvDTE.Project project, string id)
{
return _packageInstallerServices.IsPackageInstalled(project, id);
}
public void InstallPackage(string source, EnvDTE.Project project, string packageId, string version, bool ignoreDependencies)
{
_packageInstaller.InstallPackage(source, project, packageId, version, ignoreDependencies);
}
public event EventHandler SourcesChanged
{
add
{
_packageSourceProvider.SourcesChanged += value;
}
remove
{
_packageSourceProvider.SourcesChanged -= value;
}
}
public IEnumerable<KeyValuePair<string, string>> GetSources(bool includeUnOfficial, bool includeDisabled)
{
return _packageSourceProvider.GetSources(includeUnOfficial, includeDisabled);
}
public void UninstallPackage(EnvDTE.Project project, string packageId, bool removeDependencies)
{
_packageUninstaller.UninstallPackage(project, packageId, removeDependencies);
}
}
}
}
\ No newline at end of file
......@@ -155,6 +155,8 @@
<Compile Include="Implementation\Workspace\VisualStudioProjectCacheHostServiceFactory.cs" />
<Compile Include="IRoslynTelemetrySetup.cs" />
<Compile Include="Packaging\Interop\SVsRemoteControlService.cs" />
<Compile Include="Packaging\IPackageServicesProxy.cs" />
<Compile Include="SymbolSearch\AbstractDelayStartedService.cs" />
<Compile Include="SymbolSearch\IAddReferenceDatabaseWrapper.cs" />
<Compile Include="SymbolSearch\IDatabaseFactoryService.cs" />
<Compile Include="SymbolSearch\IDelayService.cs" />
......
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Options;
namespace Microsoft.VisualStudio.LanguageServices.SymbolSearch
{
/// <summary>
/// Base type for services that we want to delay running until certain criteria is met.
/// For example, we don't want to run the <see cref="SymbolSearchService"/> core codepath
/// if the user has not enabled the features that need it. That helps us avoid loading
/// dlls unnecessarily and bloating the VS memory space.
/// </summary>
internal abstract class AbstractDelayStartedService : ForegroundThreadAffinitizedObject
{
private readonly List<string> _registeredLanguageNames = new List<string>();
protected readonly Workspace Workspace;
// Option that controls if this service is enabled or not (regardless of language).
private readonly Option<bool> _serviceOnOffOption;
// Options that control if this service is enabled or not for a particular language.
private readonly ImmutableArray<PerLanguageOption<bool>> _perLanguageOptions;
private bool _enabled = false;
protected AbstractDelayStartedService(
Workspace workspace,
Option<bool> onOffOption,
params PerLanguageOption<bool>[] perLanguageOptions)
{
Workspace = workspace;
_serviceOnOffOption = onOffOption;
_perLanguageOptions = perLanguageOptions.ToImmutableArray();
}
protected abstract void EnableService();
protected abstract void StartWorking();
protected abstract void StopWorking();
internal void Connect(string languageName)
{
this.AssertIsForeground();
var options = Workspace.Options;
if (!options.GetOption(_serviceOnOffOption))
{
// Feature is totally disabled. Do nothing.
return;
}
this._registeredLanguageNames.Add(languageName);
if (this._registeredLanguageNames.Count == 1)
{
// Register to hear about option changing.
var optionsService = Workspace.Services.GetService<IOptionService>();
optionsService.OptionChanged += OnOptionChanged;
}
// Kick things off.
OnOptionChanged(this, EventArgs.Empty);
}
private void OnOptionChanged(object sender, EventArgs e)
{
this.AssertIsForeground();
if (!_registeredLanguageNames.Any(IsRegisteredForLanguage))
{
// The feature is not enabled for any registered languages.
return;
}
// The first time we see that we're registered for a language, enable the
// service.
if (!_enabled)
{
_enabled = true;
EnableService();
}
// Then tell it to start work.
StartWorking();
}
private bool IsRegisteredForLanguage(string language)
{
var options = Workspace.Options;
return _perLanguageOptions.Any(o => options.GetOption(o, language));
}
internal void Disconnect(string languageName)
{
this.AssertIsForeground();
var options = Workspace.Options;
if (!options.GetOption(_serviceOnOffOption))
{
// Feature is totally disabled. Do nothing.
return;
}
_registeredLanguageNames.Remove(languageName);
if (_registeredLanguageNames.Count == 0)
{
if (_enabled)
{
_enabled = false;
StopWorking();
}
var optionsService = Workspace.Services.GetService<IOptionService>();
optionsService.OptionChanged -= OnOptionChanged;
}
}
}
}
\ No newline at end of file
......@@ -5,17 +5,14 @@
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Elfie.Model;
using Microsoft.CodeAnalysis.Packaging;
using Microsoft.CodeAnalysis.Shared.Options;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Microsoft.Internal.VisualStudio.Shell.Interop;
using Roslyn.Utilities;
......@@ -69,36 +66,6 @@ internal partial class SymbolSearchService
private void LogException(Exception e, string text) => _logService.LogException(e, text);
private void OnOptionChanged(object sender, EventArgs e)
{
// If we don't have any add-import features that would use these indices, then
// don't bother creating them.
if (!_registeredLanguageNames.Any(IsRegisteredForLanguage))
{
return;
}
// Kick off a database update. Wait a few seconds before starting so we don't
// interfere too much with solution loading.
var sources = _installerService.PackageSources;
// Always pull down the nuget.org index. It contains the MS reference assembly index
// inside of it.
var allSources = sources.Concat(new PackageSource(NugetOrgSource, source: null));
foreach (var source in allSources)
{
Task.Delay(TimeSpan.FromSeconds(10)).ContinueWith(_ =>
UpdateSourceInBackgroundAsync(source.Name), TaskScheduler.Default);
}
}
private bool IsRegisteredForLanguage(string language)
{
var options = _workspace.Options;
return options.GetOption(AddImportOptions.SuggestForTypesInReferenceAssemblies, language) ||
options.GetOption(AddImportOptions.SuggestForTypesInNuGetPackages, language);
}
// internal for testing purposes.
internal Task UpdateSourceInBackgroundAsync(string source)
......
......@@ -6,8 +6,8 @@
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Editor.Shared.Utilities;
using Microsoft.CodeAnalysis.Elfie.Model;
using Microsoft.CodeAnalysis.Elfie.Model.Structures;
using Microsoft.CodeAnalysis.Elfie.Model.Tree;
......@@ -15,13 +15,13 @@
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.Packaging;
using Microsoft.CodeAnalysis.Shared.Options;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.SymbolSearch;
using Microsoft.Internal.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.LanguageServices.Implementation.ProjectSystem;
using Microsoft.VisualStudio.Settings;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.Shell.Settings;
using Roslyn.Utilities;
using VSShell = Microsoft.VisualStudio.Shell;
namespace Microsoft.VisualStudio.LanguageServices.SymbolSearch
......@@ -34,15 +34,13 @@ namespace Microsoft.VisualStudio.LanguageServices.SymbolSearch
/// date by downloading patches on a daily basis.
/// </summary>
[ExportWorkspaceService(typeof(ISymbolSearchService)), Shared]
internal partial class SymbolSearchService : ForegroundThreadAffinitizedObject, ISymbolSearchService
internal partial class SymbolSearchService : AbstractDelayStartedService, ISymbolSearchService
{
private readonly Workspace _workspace;
private ConcurrentDictionary<string, IAddReferenceDatabaseWrapper> _sourceToDatabase =
new ConcurrentDictionary<string, IAddReferenceDatabaseWrapper>();
private readonly List<string> _registeredLanguageNames = new List<string>();
[ImportingConstructor]
public SymbolSearchService(
VisualStudioWorkspaceImpl workspace,
......@@ -88,7 +86,10 @@ private static IRemoteControlService CreateRemoteControlService(VSShell.SVsServi
IDatabaseFactoryService databaseFactoryService,
string localSettingsDirectory,
Func<Exception, bool> reportAndSwallowException,
CancellationTokenSource cancellationTokenSource)
CancellationTokenSource cancellationTokenSource)
: base(workspace, ServiceComponentOnOffOptions.SymbolSearch,
AddImportOptions.SuggestForTypesInReferenceAssemblies,
AddImportOptions.SuggestForTypesInNuGetPackages)
{
if (remoteControlService == null)
{
......@@ -111,46 +112,40 @@ private static IRemoteControlService CreateRemoteControlService(VSShell.SVsServi
_cancellationToken = _cancellationTokenSource.Token;
}
internal void Start(string languageName)
protected override void EnableService()
{
_registeredLanguageNames.Add(languageName);
if (_registeredLanguageNames.Count == 1)
{
// When the first language registers, start the service.
var options = _workspace.Options;
if (!options.GetOption(ServiceComponentOnOffOptions.SymbolSearch))
{
return;
}
var optionsService = _workspace.Services.GetService<IOptionService>();
optionsService.OptionChanged += OnOptionChanged;
// Start the whole process once we're connected
_installerService.PackageSourcesChanged += OnOptionChanged;
}
// When our service is enabled hook up to package source changes.
// We need to know when the list of sources have changed so we can
// kick off the work to process them.
_installerService.PackageSourcesChanged += OnPackageSourcesChanged;
}
// Kick things off.
OnOptionChanged(this, EventArgs.Empty);
private void OnPackageSourcesChanged(object sender, EventArgs e)
{
StartWorking();
}
internal void Stop(string languageName)
protected override void StartWorking()
{
_registeredLanguageNames.Remove(languageName);
if (_registeredLanguageNames.Count == 0)
// Kick off a database update. Wait a few seconds before starting so we don't
// interfere too much with solution loading.
var sources = _installerService.PackageSources;
// Always pull down the nuget.org index. It contains the MS reference assembly index
// inside of it.
var allSources = sources.Concat(new PackageSource(NugetOrgSource, source: null));
foreach (var source in allSources)
{
// once there are no more languages registered, we can actually stop this service.
var optionsService = _workspace.Services.GetService<IOptionService>();
optionsService.OptionChanged -= OnOptionChanged;
_installerService.PackageSourcesChanged -= OnOptionChanged;
// Cancel any existing work.
_cancellationTokenSource.Cancel();
Task.Run(() => UpdateSourceInBackgroundAsync(source.Name));
}
}
protected override void StopWorking()
{
_installerService.PackageSourcesChanged -= OnPackageSourcesChanged;
_cancellationTokenSource.Cancel();
}
public IEnumerable<PackageWithTypeResult> FindPackagesWithType(
string source, string name, int arity, CancellationToken cancellationToken)
{
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册