diff --git a/.gitignore b/.gitignore index 871fd03..95d4a30 100644 --- a/.gitignore +++ b/.gitignore @@ -347,7 +347,4 @@ healthchecksdb MigrationBackup/ # Ionide (cross platform F# VS Code tools) working folder -.ionide/ - -# Signing certificates -*.snk \ No newline at end of file +.ionide/ \ No newline at end of file diff --git a/SolutionResources/.gitignore b/SolutionResources/.gitignore new file mode 100644 index 0000000..2c7ef37 --- /dev/null +++ b/SolutionResources/.gitignore @@ -0,0 +1,4 @@ +ValheimServerGUI.snk +appsettings.secret.json +appsettings.local.json +Secrets.Values.cs \ No newline at end of file diff --git a/SolutionResources/README.md b/SolutionResources/README.md new file mode 100644 index 0000000..cb3bcad --- /dev/null +++ b/SolutionResources/README.md @@ -0,0 +1,6 @@ +# Solution Resources + +### Developer note + +You must include the files outlined in the .gitignore in order to properly build and publish this project. +Contact Runeberry Software for more information. \ No newline at end of file diff --git a/SolutionResources/Secrets.cs b/SolutionResources/Secrets.cs new file mode 100644 index 0000000..58b08b2 --- /dev/null +++ b/SolutionResources/Secrets.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; + +namespace ValheimServerGUI.Properties +{ + /// + /// The values in this class are populated by the static constructor in the corresponding + /// partial class, which is kept out of source control. + /// + public static partial class Secrets + { + /// + /// The HTTP header that will contain the API key for requests made to the Runeberry API. + /// + public static string RuneberryApiKeyHeader { get; } = string.Empty; + + /// + /// The API key that will be attached to all VSG client requests to the Runeberry API. + /// + public static string RuneberryClientApiKey { get; } + + /// + /// The Runeberry API keys that will be accepted by the server. + /// + public static HashSet RuneberryServerApiKeys { get; } = new(); + } +} diff --git a/ValheimServerGUI.Controls/Controls/TextFormField.Designer.cs b/ValheimServerGUI.Controls/Controls/TextFormField.Designer.cs index 97e63ec..bfadd50 100644 --- a/ValheimServerGUI.Controls/Controls/TextFormField.Designer.cs +++ b/ValheimServerGUI.Controls/Controls/TextFormField.Designer.cs @@ -36,7 +36,8 @@ private void InitializeComponent() // // TextBox // - this.TextBox.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + this.TextBox.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right))); this.TextBox.Location = new System.Drawing.Point(9, 18); this.TextBox.Name = "TextBox"; diff --git a/ValheimServerGUI.Controls/Controls/TextFormField.cs b/ValheimServerGUI.Controls/Controls/TextFormField.cs index 10b2479..028d328 100644 --- a/ValheimServerGUI.Controls/Controls/TextFormField.cs +++ b/ValheimServerGUI.Controls/Controls/TextFormField.cs @@ -52,6 +52,12 @@ public int MaxLength set => this.TextBox.MaxLength = value; } + public bool Multiline + { + get => this.TextBox.Multiline; + set => this.TextBox.Multiline = value; + } + public TextFormField() { InitializeComponent(); diff --git a/ValheimServerGUI.Controls/ValheimServerGUI.Controls.csproj b/ValheimServerGUI.Controls/ValheimServerGUI.Controls.csproj index 6108a98..27d3e64 100644 --- a/ValheimServerGUI.Controls/ValheimServerGUI.Controls.csproj +++ b/ValheimServerGUI.Controls/ValheimServerGUI.Controls.csproj @@ -4,8 +4,10 @@ net5.0-windows true ValheimServerGUI + + true - ValheimServerGUI.snk + ..\SolutionResources\ValheimServerGUI.snk diff --git a/ValheimServerGUI.Serverless.Tests/Properties/launchSettings.json b/ValheimServerGUI.Serverless.Tests/Properties/launchSettings.json new file mode 100644 index 0000000..d8d7436 --- /dev/null +++ b/ValheimServerGUI.Serverless.Tests/Properties/launchSettings.json @@ -0,0 +1,14 @@ +{ + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "ValheimServerGUI.Serverless.Tests": { + "commandName": "Project" + } + } +} \ No newline at end of file diff --git a/ValheimServerGUI.Serverless.Tests/SampleRequests/ValuesController-Get.json b/ValheimServerGUI.Serverless.Tests/SampleRequests/ValuesController-Get.json new file mode 100644 index 0000000..e624852 --- /dev/null +++ b/ValheimServerGUI.Serverless.Tests/SampleRequests/ValuesController-Get.json @@ -0,0 +1,34 @@ +{ + "resource": "/{proxy+}", + "path": "/api/values", + "httpMethod": "GET", + "headers": null, + "queryStringParameters": null, + "pathParameters": { + "proxy": "api/values" + }, + "stageVariables": null, + "requestContext": { + "accountId": "AAAAAAAAAAAA", + "resourceId": "5agfss", + "stage": "test-invoke-stage", + "requestId": "test-invoke-request", + "identity": { + "cognitoIdentityPoolId": null, + "accountId": "AAAAAAAAAAAA", + "cognitoIdentityId": null, + "caller": "BBBBBBBBBBBB", + "apiKey": "test-invoke-api-key", + "sourceIp": "test-invoke-source-ip", + "cognitoAuthenticationType": null, + "cognitoAuthenticationProvider": null, + "userArn": "arn:aws:iam::AAAAAAAAAAAA:root", + "userAgent": "Apache-HttpClient/4.5.x (Java/1.8.0_102)", + "user": "AAAAAAAAAAAA" + }, + "resourcePath": "/{proxy+}", + "httpMethod": "GET", + "apiId": "t2yh6sjnmk" + }, + "body": null +} \ No newline at end of file diff --git a/ValheimServerGUI.Serverless.Tests/ValheimServerGUI.Serverless.Tests.csproj b/ValheimServerGUI.Serverless.Tests/ValheimServerGUI.Serverless.Tests.csproj new file mode 100644 index 0000000..f29d117 --- /dev/null +++ b/ValheimServerGUI.Serverless.Tests/ValheimServerGUI.Serverless.Tests.csproj @@ -0,0 +1,32 @@ + + + net5.0 + False + + + + PreserveNewest + + + + + PreserveNewest + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + \ No newline at end of file diff --git a/ValheimServerGUI.Serverless.Tests/ValuesControllerTests.cs b/ValheimServerGUI.Serverless.Tests/ValuesControllerTests.cs new file mode 100644 index 0000000..3d849b6 --- /dev/null +++ b/ValheimServerGUI.Serverless.Tests/ValuesControllerTests.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; + +using Xunit; +using Amazon.Lambda.Core; +using Amazon.Lambda.TestUtilities; +using Amazon.Lambda.APIGatewayEvents; + +using Newtonsoft.Json; + +using ValheimServerGUI.Serverless; + + +namespace ValheimServerGUI.Serverless.Tests +{ + public class ValuesControllerTests + { + + + [Fact] + public async Task TestGet() + { + var lambdaFunction = new LambdaEntryPoint(); + + var requestStr = File.ReadAllText("./SampleRequests/ValuesController-Get.json"); + var request = JsonConvert.DeserializeObject(requestStr); + var context = new TestLambdaContext(); + var response = await lambdaFunction.FunctionHandlerAsync(request, context); + + Assert.Equal(200, response.StatusCode); + Assert.Equal("[\"value1\",\"value2\"]", response.Body); + Assert.True(response.MultiValueHeaders.ContainsKey("Content-Type")); + Assert.Equal("application/json; charset=utf-8", response.MultiValueHeaders["Content-Type"][0]); + } + + + } +} diff --git a/ValheimServerGUI.Serverless.Tests/appsettings.json b/ValheimServerGUI.Serverless.Tests/appsettings.json new file mode 100644 index 0000000..2283363 --- /dev/null +++ b/ValheimServerGUI.Serverless.Tests/appsettings.json @@ -0,0 +1,14 @@ +{ + "Lambda.Logging": { + "IncludeCategory": false, + "IncludeLogLevel": false, + "IncludeNewline": true, + "LogLevel": { + "Default": "Debug", + "Microsoft": "Information" + } + }, + "AWS": { + "Region": "DefaultRegion" + } +} \ No newline at end of file diff --git a/ValheimServerGUI.Serverless/Controllers/VsgController.cs b/ValheimServerGUI.Serverless/Controllers/VsgController.cs new file mode 100644 index 0000000..02385b2 --- /dev/null +++ b/ValheimServerGUI.Serverless/Controllers/VsgController.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Amazon; +using Amazon.Lambda.Core; +using Amazon.S3; +using Amazon.S3.Model; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using ValheimServerGUI.Tools; + +namespace ValheimServerGUI.Serverless.Controllers +{ + [ApiController] + public class VsgController : ControllerBase + { + private readonly ILogger Logger; + private readonly IConfiguration Configuration; + + private ILambdaContext LambdaContext => this.HttpContext.Items["LambdaContext"] as ILambdaContext; + + public VsgController(ILogger logger, IConfiguration configuration) + { + Logger = logger; + Configuration = configuration; + } + + [HttpPost("crash-report")] + public async Task CreateCrashReport([FromBody] CrashReport request) + { + Logger.LogInformation("Receiving crash report (standard logger)"); + + Exception exception; + int statusCode; + + try + { + var s3BucketName = Configuration.GetValue("S3BucketName"); + var s3BucketRegion = Configuration.GetValue("S3BucketRegion"); + + var client = new AmazonS3Client(RegionEndpoint.GetBySystemName(s3BucketRegion)); + + // Ensure that each crash report has an ID + request.CrashReportId ??= Guid.NewGuid().ToString(); + request.Source ??= "CrashReport"; + request.Timestamp ??= DateTimeOffset.UtcNow; + var filename = $"{request.Source}-{request.Timestamp.Value.ToFileTime()}-{request.CrashReportId}.json"; + + var s3Request = new PutObjectRequest + { + BucketName = s3BucketName, + Key = $"crash-reports/{filename}", + ContentType = "application/json", + ContentBody = JsonConvert.SerializeObject(request), + }; + var s3Response = await client.PutObjectAsync(s3Request); + + Logger.LogInformation($"Crash report created: {request.CrashReportId}"); + + return Accepted(request); + } + catch (AmazonS3Exception e) + { + exception = e; + statusCode = (int)e.StatusCode; + } + catch (Exception e) + { + exception = e; + statusCode = 500; + } + + Logger.LogException(exception, $"{exception.GetType().Name} occurred during S3 upload"); + Logger.LogError(exception.Message); + Logger.LogError(exception.StackTrace); + + return StatusCode(statusCode, new { message = exception.Message }); + } + + [HttpGet("player-steam-info")] + public async Task GetPlayerSteamInfo([FromQuery] string steamId) + { + return Ok("Player steam info"); + } + } +} diff --git a/ValheimServerGUI.Serverless/Dockerfile b/ValheimServerGUI.Serverless/Dockerfile new file mode 100644 index 0000000..68ec18f --- /dev/null +++ b/ValheimServerGUI.Serverless/Dockerfile @@ -0,0 +1,13 @@ +FROM public.ecr.aws/lambda/dotnet:5.0 + +WORKDIR /var/task + +# This COPY command copies the .NET Lambda project's build artifacts from the host machine into the image. +# The source of the COPY should match where the .NET Lambda project publishes its build artifacts. If the Lambda function is being built +# with the AWS .NET Lambda Tooling, the `--docker-host-build-output-dir` switch controls where the .NET Lambda project +# will be built. The .NET Lambda project templates default to having `--docker-host-build-output-dir` +# set in the aws-lambda-tools-defaults.json file to "bin/Release/net5.0/linux-x64/publish". +# +# Alternatively Docker multi-stage build could be used to build the .NET Lambda project inside the image. +# For more information on this approach checkout the project's README.md file. +COPY "bin/Release/net5.0/linux-x64/publish" . diff --git a/ValheimServerGUI.Serverless/LambdaEntryPoint.cs b/ValheimServerGUI.Serverless/LambdaEntryPoint.cs new file mode 100644 index 0000000..649c296 --- /dev/null +++ b/ValheimServerGUI.Serverless/LambdaEntryPoint.cs @@ -0,0 +1,57 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; + +namespace ValheimServerGUI.Serverless +{ + /// + /// This class extends from APIGatewayProxyFunction which contains the method FunctionHandlerAsync which is the + /// actual Lambda function entry point. The Lambda handler field should be set to + /// + /// ValheimServerGUI.Serverless::ValheimServerGUI.Serverless.LambdaEntryPoint::FunctionHandlerAsync + /// + public class LambdaEntryPoint : + + // The base class must be set to match the AWS service invoking the Lambda function. If not Amazon.Lambda.AspNetCoreServer + // will fail to convert the incoming request correctly into a valid ASP.NET Core request. + // + // API Gateway REST API -> Amazon.Lambda.AspNetCoreServer.APIGatewayProxyFunction + // API Gateway HTTP API payload version 1.0 -> Amazon.Lambda.AspNetCoreServer.APIGatewayProxyFunction + // API Gateway HTTP API payload version 2.0 -> Amazon.Lambda.AspNetCoreServer.APIGatewayHttpApiV2ProxyFunction + // Application Load Balancer -> Amazon.Lambda.AspNetCoreServer.ApplicationLoadBalancerFunction + // + // Note: When using the AWS::Serverless::Function resource with an event type of "HttpApi" then payload version 2.0 + // will be the default and you must make Amazon.Lambda.AspNetCoreServer.APIGatewayHttpApiV2ProxyFunction the base class. + + Amazon.Lambda.AspNetCoreServer.APIGatewayProxyFunction + { + /// + /// The builder has configuration, logging and Amazon API Gateway already configured. The startup class + /// needs to be configured in this method using the UseStartup<>() method. + /// + /// + protected override void Init(IWebHostBuilder builder) + { + builder + .ConfigureAppConfiguration(config => + { + config.AddJsonFile("appsettings.secret.json"); + }) + .UseStartup(); + } + + /// + /// Use this override to customize the services registered with the IHostBuilder. + /// + /// It is recommended not to call ConfigureWebHostDefaults to configure the IWebHostBuilder inside this method. + /// Instead customize the IWebHostBuilder in the Init(IWebHostBuilder) overload. + /// + /// + protected override void Init(IHostBuilder builder) + { + } + } +} diff --git a/ValheimServerGUI.Serverless/LocalEntryPoint.cs b/ValheimServerGUI.Serverless/LocalEntryPoint.cs new file mode 100644 index 0000000..64df239 --- /dev/null +++ b/ValheimServerGUI.Serverless/LocalEntryPoint.cs @@ -0,0 +1,33 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; + +namespace ValheimServerGUI.Serverless +{ + /// + /// The Main function can be used to run the ASP.NET Core application locally using the Kestrel webserver. + /// + public class LocalEntryPoint + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.ConfigureAppConfiguration(config => + { + var executingLocation = System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location); + config.AddJsonFile(System.IO.Path.Join(executingLocation, "appsettings.secret.json")); + config.AddJsonFile(System.IO.Path.Join(executingLocation, "appsettings.local.json"), optional: true); + }); + webBuilder.UseStartup(); + }); + } +} diff --git a/ValheimServerGUI.Serverless/Middleware/RuneberryAuthMiddleware.cs b/ValheimServerGUI.Serverless/Middleware/RuneberryAuthMiddleware.cs new file mode 100644 index 0000000..0fde96c --- /dev/null +++ b/ValheimServerGUI.Serverless/Middleware/RuneberryAuthMiddleware.cs @@ -0,0 +1,42 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using ValheimServerGUI.Properties; + +namespace ValheimServerGUI.Serverless.Middleware +{ + [ApiController] + public class RuneberryAuthMiddleware + { + private static readonly bool ApiKeyEnabled; + + static RuneberryAuthMiddleware() + { + ApiKeyEnabled = !string.IsNullOrWhiteSpace(Secrets.RuneberryApiKeyHeader) && Secrets.RuneberryApiKeyHeader.Any(); + } + + public static async Task Authorize(HttpContext context, Func next) + { + if (ApiKeyEnabled) + { + if (!context.Request.Headers.TryGetValue(Secrets.RuneberryApiKeyHeader, out var apiKey)) + { + context.Response.StatusCode = 401; + await context.Response.WriteAsJsonAsync(new { message = "Missing API key" }); + return; + } + + if (!Secrets.RuneberryServerApiKeys.Contains(apiKey)) + { + context.Response.StatusCode = 401; + await context.Response.WriteAsJsonAsync(new { message = "Invalid API key" }); + return; + } + } + + await next?.Invoke(); + } + } +} diff --git a/ValheimServerGUI.Serverless/Properties/launchSettings.json b/ValheimServerGUI.Serverless/Properties/launchSettings.json new file mode 100644 index 0000000..1844dff --- /dev/null +++ b/ValheimServerGUI.Serverless/Properties/launchSettings.json @@ -0,0 +1,20 @@ +{ + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "ValheimServerGUI.Serverless": { + "commandName": "Project" + }, + "Mock Lambda Test Tool": { + "commandName": "Executable", + "commandLineArgs": "--port 5050", + "workingDirectory": ".\\bin\\$(Configuration)\\net5.0", + "executablePath": "%USERPROFILE%\\.dotnet\\tools\\dotnet-lambda-test-tool-5.0.exe" + } + } +} \ No newline at end of file diff --git a/ValheimServerGUI.Serverless/Readme.md b/ValheimServerGUI.Serverless/Readme.md new file mode 100644 index 0000000..a57f49e --- /dev/null +++ b/ValheimServerGUI.Serverless/Readme.md @@ -0,0 +1,108 @@ +# ASP.NET Core Web API Serverless Application + +This project shows how to run an ASP.NET Core Web API project as an AWS Lambda exposed through Amazon API Gateway. The NuGet package [Amazon.Lambda.AspNetCoreServer](https://www.nuget.org/packages/Amazon.Lambda.AspNetCoreServer) contains a Lambda function that is used to translate requests from API Gateway into the ASP.NET Core framework and then the responses from ASP.NET Core back to API Gateway. + + +For more information about how the Amazon.Lambda.AspNetCoreServer package works and how to extend its behavior view its [README](https://github.com/aws/aws-lambda-dotnet/blob/master/Libraries/src/Amazon.Lambda.AspNetCoreServer/README.md) file in GitHub. + + +### Configuring for API Gateway HTTP API ### + +API Gateway supports the original REST API and the new HTTP API. In addition HTTP API supports 2 different +payload formats. When using the 2.0 format the base class of `LambdaEntryPoint` must be `Amazon.Lambda.AspNetCoreServer.APIGatewayHttpApiV2ProxyFunction`. +For the 1.0 payload format the base class is the same as REST API which is `Amazon.Lambda.AspNetCoreServer.APIGatewayProxyFunction`. +**Note:** when using the `AWS::Serverless::Function` CloudFormation resource with an event type of `HttpApi` the default payload +format is 2.0 so the base class of `LambdaEntryPoint` must be `Amazon.Lambda.AspNetCoreServer.APIGatewayHttpApiV2ProxyFunction`. + + +### Configuring for Application Load Balancer ### + +To configure this project to handle requests from an Application Load Balancer instead of API Gateway change +the base class of `LambdaEntryPoint` from `Amazon.Lambda.AspNetCoreServer.APIGatewayProxyFunction` to +`Amazon.Lambda.AspNetCoreServer.ApplicationLoadBalancerFunction`. + +### Project Files ### + +* serverless.template - an AWS CloudFormation Serverless Application Model template file for declaring your Serverless functions and other AWS resources +* aws-lambda-tools-defaults.json - default argument settings for use with Visual Studio and command line deployment tools for AWS +* LambdaEntryPoint.cs - class that derives from **Amazon.Lambda.AspNetCoreServer.APIGatewayProxyFunction**. The code in +this file bootstraps the ASP.NET Core hosting framework. The Lambda function is defined in the base class. +Change the base class to **Amazon.Lambda.AspNetCoreServer.ApplicationLoadBalancerFunction** when using an +Application Load Balancer. +* LocalEntryPoint.cs - for local development this contains the executable Main function which bootstraps the ASP.NET Core hosting framework with Kestrel, as for typical ASP.NET Core applications. +* Startup.cs - usual ASP.NET Core Startup class used to configure the services ASP.NET Core will use. +* web.config - used for local development. +* Controllers\ValuesController - example Web API controller + +You may also have a test project depending on the options selected. + +## Packaging as a Docker image. + +This project is configured to package the Lambda function as a Docker image. The default configuration for the project and the Dockerfile is to build +the .NET project on the host machine and then execute the `docker build` command which copies the .NET build artifacts from the host machine into +the Docker image. + +The `--docker-host-build-output-dir` switch, which is set in the `aws-lambda-tools-defaults.json`, triggers the +AWS .NET Lambda tooling to build the .NET project into the directory indicated by `--docker-host-build-output-dir`. The Dockerfile +has a **COPY** command which copies the value from the directory pointed to by `--docker-host-build-output-dir` to the `/var/task` directory inside of the +image. + +Alternatively the Docker file could be written to use [multi-stage](https://docs.docker.com/develop/develop-images/multistage-build/) builds and +have the .NET project built inside the container. Below is an example of building .NET 5 project inside the image. + +```dockerfile +FROM ecr.aws/lambda/dotnet:5.0 AS base + +FROM mcr.microsoft.com/dotnet/sdk:5.0-buster-slim as build +WORKDIR /src +COPY ["ValheimServerGUI.Serverless.csproj", "ValheimServerGUI.Serverless/"] +RUN dotnet restore "ValheimServerGUI.Serverless/ValheimServerGUI.Serverless.csproj" + +WORKDIR "/src/ValheimServerGUI.Serverless" +COPY . . +RUN dotnet build "ValheimServerGUI.Serverless.csproj" --configuration Release --output /app/build + +FROM build AS publish +RUN dotnet publish "ValheimServerGUI.Serverless.csproj" \ + --configuration Release \ + --runtime linux-x64 \ + --self-contained false \ + --output /app/publish \ + -p:PublishReadyToRun=true + +FROM base AS final +WORKDIR /var/task +COPY --from=publish /app/publish . +``` + +## Here are some steps to follow from Visual Studio: + +To deploy your Serverless application, right click the project in Solution Explorer and select *Publish to AWS Lambda*. + +To view your deployed application open the Stack View window by double-clicking the stack name shown beneath the AWS CloudFormation node in the AWS Explorer tree. The Stack View also displays the root URL to your published application. + +## Here are some steps to follow to get started from the command line: + +Once you have edited your template and code you can deploy your application using the [Amazon.Lambda.Tools Global Tool](https://github.com/aws/aws-extensions-for-dotnet-cli#aws-lambda-amazonlambdatools) from the command line. + +Install Amazon.Lambda.Tools Global Tools if not already installed. +``` + dotnet tool install -g Amazon.Lambda.Tools +``` + +If already installed check if new version is available. +``` + dotnet tool update -g Amazon.Lambda.Tools +``` + +Execute unit tests +``` + cd "ValheimServerGUI.Serverless/test/ValheimServerGUI.Serverless.Tests" + dotnet test +``` + +Deploy application +``` + cd "ValheimServerGUI.Serverless/src/ValheimServerGUI.Serverless" + dotnet lambda deploy-serverless +``` diff --git a/ValheimServerGUI.Serverless/Startup.cs b/ValheimServerGUI.Serverless/Startup.cs new file mode 100644 index 0000000..9265370 --- /dev/null +++ b/ValheimServerGUI.Serverless/Startup.cs @@ -0,0 +1,52 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using ValheimServerGUI.Serverless.Middleware; + +namespace ValheimServerGUI.Serverless +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public static IConfiguration Configuration { get; private set; } + + // This method gets called by the runtime. Use this method to add services to the container + public void ConfigureServices(IServiceCollection services) + { + services.AddControllers(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseHttpsRedirection(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.Use(RuneberryAuthMiddleware.Authorize); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + endpoints.MapGet("/", async context => + { + await context.Response.WriteAsync("Welcome to running ASP.NET Core on AWS Lambda"); + }); + }); + } + } +} diff --git a/ValheimServerGUI.Serverless/ValheimServerGUI.Serverless.csproj b/ValheimServerGUI.Serverless/ValheimServerGUI.Serverless.csproj new file mode 100644 index 0000000..8b38f55 --- /dev/null +++ b/ValheimServerGUI.Serverless/ValheimServerGUI.Serverless.csproj @@ -0,0 +1,28 @@ + + + net5.0 + true + Lambda + + true + + + + + + + + + + + + + + + + + %(RecursiveDir)%(Filename)%(Extension) + PreserveNewest + + + \ No newline at end of file diff --git a/ValheimServerGUI.Serverless/appsettings.Development.json b/ValheimServerGUI.Serverless/appsettings.Development.json new file mode 100644 index 0000000..4c50c0a --- /dev/null +++ b/ValheimServerGUI.Serverless/appsettings.Development.json @@ -0,0 +1,5 @@ +{ + "AWS": { + "Region": "DefaultRegion" + } +} \ No newline at end of file diff --git a/ValheimServerGUI.Serverless/appsettings.json b/ValheimServerGUI.Serverless/appsettings.json new file mode 100644 index 0000000..5e78dfb --- /dev/null +++ b/ValheimServerGUI.Serverless/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information" + } + }, + "S3BucketName": "runeberry-valheim-server-gui", + "S3BucketRegion": "us-east-1" +} \ No newline at end of file diff --git a/ValheimServerGUI.Serverless/aws-lambda-tools-defaults.json b/ValheimServerGUI.Serverless/aws-lambda-tools-defaults.json new file mode 100644 index 0000000..dca533c --- /dev/null +++ b/ValheimServerGUI.Serverless/aws-lambda-tools-defaults.json @@ -0,0 +1,18 @@ + +{ + "Information" : [ + "This file provides default values for the deployment wizard inside Visual Studio and the AWS Lambda commands added to the .NET Core CLI.", + "To learn more about the Lambda commands with the .NET Core CLI execute the following command at the command line in the project root directory.", + "dotnet lambda help", + "All the command line options for the Lambda command can be specified in this file." + ], + "profile" : "default", + "region" : "us-east-1", + "configuration" : "Release", + "s3-prefix" : "ValheimServerGUI.Serverless/", + "template" : "serverless.template", + "template-parameters" : "", + "docker-host-build-output-dir" : "./bin/Release/net5.0/linux-x64/publish", + "s3-bucket" : "runeberry-cf-templates", + "stack-name" : "valheim-server-gui-stack" +} \ No newline at end of file diff --git a/ValheimServerGUI.Serverless/serverless.template b/ValheimServerGUI.Serverless/serverless.template new file mode 100644 index 0000000..306b141 --- /dev/null +++ b/ValheimServerGUI.Serverless/serverless.template @@ -0,0 +1,71 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Transform": "AWS::Serverless-2016-10-31", + "Description": "An AWS Serverless Application that uses the ASP.NET Core framework running in Amazon Lambda.", + "Parameters": {}, + "Conditions": {}, + "Resources": { + "AspNetCoreFunction": { + "Type": "AWS::Serverless::Function", + "Properties": { + "PackageType": "Image", + "ImageConfig": { + "EntryPoint": [ + "/lambda-entrypoint.sh" + ], + "Command": [ + "ValheimServerGUI.Serverless::ValheimServerGUI.Serverless.LambdaEntryPoint::FunctionHandlerAsync" + ] + }, + "ImageUri": "", + "MemorySize": 256, + "Timeout": 30, + "Role": null, + "Policies": [ + "AWSLambda_FullAccess", + "AmazonS3FullAccess", + "CloudWatchLambdaInsightsExecutionRolePolicy" + ], + "Events": { + "ProxyResource": { + "Type": "Api", + "Properties": { + "Path": "/{proxy+}", + "Method": "ANY" + } + }, + "RootResource": { + "Type": "Api", + "Properties": { + "Path": "/", + "Method": "ANY" + } + } + } + }, + "Metadata": { + "Dockerfile": "Dockerfile", + "DockerContext": ".", + "DockerTag": "" + } + }, + "S3Bucket": { + "Type": "AWS::S3::Bucket", + "DeletionPolicy": "Retain", + "Properties": { + "BucketName": "runeberry-valheim-server-gui", + "VersioningConfiguration": { + "Status": "Enabled" + } + } + } + }, + "Outputs": { + "ApiURL": { + "Description": "API endpoint URL for Prod environment", + "Value": { + "Fn::Sub": "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/" + } + } + } +} \ No newline at end of file diff --git a/ValheimServerGUI.Tests/ValheimServerGUI.Tests.csproj b/ValheimServerGUI.Tests/ValheimServerGUI.Tests.csproj index 7d5a437..7fe53cc 100644 --- a/ValheimServerGUI.Tests/ValheimServerGUI.Tests.csproj +++ b/ValheimServerGUI.Tests/ValheimServerGUI.Tests.csproj @@ -7,7 +7,7 @@ - + diff --git a/ValheimServerGUI.Tools/CrashReport.cs b/ValheimServerGUI.Tools/CrashReport.cs new file mode 100644 index 0000000..499bbfa --- /dev/null +++ b/ValheimServerGUI.Tools/CrashReport.cs @@ -0,0 +1,42 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; + +namespace ValheimServerGUI.Tools +{ + public class CrashReport + { + [JsonProperty("id")] + public string CrashReportId { get; set; } + + [JsonProperty("clientCorrelationId")] + public string ClientCorrelationId { get; set; } + + [JsonProperty("source")] + public string Source { get; set; } + + [JsonProperty("timestamp")] + public DateTimeOffset? Timestamp { get; set; } + + [JsonProperty("appVersion")] + public string AppVersion { get; set; } + + [JsonProperty("osVersion")] + public string OsVersion { get; set; } + + [JsonProperty("dotnetVersion")] + public string DotnetVersion { get; set; } + + [JsonProperty("currentCulture")] + public string CurrentCulture { get; set; } + + [JsonProperty("currentUiCulture")] + public string CurrentUICulture { get; set; } + + [JsonProperty("additionalInfo")] + public Dictionary AdditionalInfo { get; set; } + + [JsonProperty("logs")] + public List Logs { get; set; } + } +} diff --git a/ValheimServerGUI/Tools/Data/DataFileProviderExtensions.cs b/ValheimServerGUI.Tools/Data/DataFileProviderExtensions.cs similarity index 100% rename from ValheimServerGUI/Tools/Data/DataFileProviderExtensions.cs rename to ValheimServerGUI.Tools/Data/DataFileProviderExtensions.cs diff --git a/ValheimServerGUI/Tools/Data/DataFileRepository.cs b/ValheimServerGUI.Tools/Data/DataFileRepository.cs similarity index 100% rename from ValheimServerGUI/Tools/Data/DataFileRepository.cs rename to ValheimServerGUI.Tools/Data/DataFileRepository.cs diff --git a/ValheimServerGUI/Tools/Data/DataFileRepositoryContext.cs b/ValheimServerGUI.Tools/Data/DataFileRepositoryContext.cs similarity index 100% rename from ValheimServerGUI/Tools/Data/DataFileRepositoryContext.cs rename to ValheimServerGUI.Tools/Data/DataFileRepositoryContext.cs diff --git a/ValheimServerGUI/Tools/Data/IDataRepository.cs b/ValheimServerGUI.Tools/Data/IDataRepository.cs similarity index 100% rename from ValheimServerGUI/Tools/Data/IDataRepository.cs rename to ValheimServerGUI.Tools/Data/IDataRepository.cs diff --git a/ValheimServerGUI/Tools/Data/IFileProvider.cs b/ValheimServerGUI.Tools/Data/IFileProvider.cs similarity index 100% rename from ValheimServerGUI/Tools/Data/IFileProvider.cs rename to ValheimServerGUI.Tools/Data/IFileProvider.cs diff --git a/ValheimServerGUI/Tools/Data/IPrimaryKeyEntity.cs b/ValheimServerGUI.Tools/Data/IPrimaryKeyEntity.cs similarity index 100% rename from ValheimServerGUI/Tools/Data/IPrimaryKeyEntity.cs rename to ValheimServerGUI.Tools/Data/IPrimaryKeyEntity.cs diff --git a/ValheimServerGUI/Tools/Data/JsonDataFile.cs b/ValheimServerGUI.Tools/Data/JsonDataFile.cs similarity index 100% rename from ValheimServerGUI/Tools/Data/JsonDataFile.cs rename to ValheimServerGUI.Tools/Data/JsonDataFile.cs diff --git a/ValheimServerGUI/Tools/Data/JsonFileProvider.cs b/ValheimServerGUI.Tools/Data/JsonFileProvider.cs similarity index 100% rename from ValheimServerGUI/Tools/Data/JsonFileProvider.cs rename to ValheimServerGUI.Tools/Data/JsonFileProvider.cs diff --git a/ValheimServerGUI/Tools/Delegates.cs b/ValheimServerGUI.Tools/Delegates.cs similarity index 100% rename from ValheimServerGUI/Tools/Delegates.cs rename to ValheimServerGUI.Tools/Delegates.cs diff --git a/ValheimServerGUI/Tools/Http/HttpClientProvider.cs b/ValheimServerGUI.Tools/Http/HttpClientProvider.cs similarity index 100% rename from ValheimServerGUI/Tools/Http/HttpClientProvider.cs rename to ValheimServerGUI.Tools/Http/HttpClientProvider.cs diff --git a/ValheimServerGUI/Tools/Http/RestClient.cs b/ValheimServerGUI.Tools/Http/RestClient.cs similarity index 85% rename from ValheimServerGUI/Tools/Http/RestClient.cs rename to ValheimServerGUI.Tools/Http/RestClient.cs index 4aca644..a4158f3 100644 --- a/ValheimServerGUI/Tools/Http/RestClient.cs +++ b/ValheimServerGUI.Tools/Http/RestClient.cs @@ -24,6 +24,11 @@ public RestClientRequest Get(string uri) return BuildRequest(HttpMethod.Get, uri); } + public RestClientRequest Post(string uri, object payload = null) + { + return BuildRequest(HttpMethod.Post, uri, payload); + } + private RestClientRequest BuildRequest(HttpMethod method, string uri, object payload = null) { return new RestClientRequest(this) diff --git a/ValheimServerGUI/Tools/Http/RestClientContext.cs b/ValheimServerGUI.Tools/Http/RestClientContext.cs similarity index 100% rename from ValheimServerGUI/Tools/Http/RestClientContext.cs rename to ValheimServerGUI.Tools/Http/RestClientContext.cs diff --git a/ValheimServerGUI/Tools/Http/RestClientRequest.cs b/ValheimServerGUI.Tools/Http/RestClientRequest.cs similarity index 94% rename from ValheimServerGUI/Tools/Http/RestClientRequest.cs rename to ValheimServerGUI.Tools/Http/RestClientRequest.cs index a176935..9ebbcbc 100644 --- a/ValheimServerGUI/Tools/Http/RestClientRequest.cs +++ b/ValheimServerGUI.Tools/Http/RestClientRequest.cs @@ -24,6 +24,8 @@ public class RestClientRequest public Type ResponseContentType { get; set; } + public List> ClientBuilders { get; } = new(); + public List> RequestBuilders { get; } = new(); public List> Callbacks { get; } = new(); @@ -47,6 +49,12 @@ public async Task SendAsync() try { var client = this.Context.HttpClientProvider.CreateClient(); + + foreach (var clientBuilder in this.ClientBuilders) + { + clientBuilder(client); + } + var requestMessage = new HttpRequestMessage(this.Method, this.Uri); if (this.RequestContent != null) diff --git a/ValheimServerGUI/Tools/Http/RestClientRequestExtensions.cs b/ValheimServerGUI.Tools/Http/RestClientRequestExtensions.cs similarity index 77% rename from ValheimServerGUI/Tools/Http/RestClientRequestExtensions.cs rename to ValheimServerGUI.Tools/Http/RestClientRequestExtensions.cs index 1a43e09..6edb01d 100644 --- a/ValheimServerGUI/Tools/Http/RestClientRequestExtensions.cs +++ b/ValheimServerGUI.Tools/Http/RestClientRequestExtensions.cs @@ -6,6 +6,18 @@ namespace ValheimServerGUI.Tools.Http { public static class RestClientRequestExtensions { + public static RestClientRequest WithClientOptions(this RestClientRequest request, Action options) + { + request.ClientBuilders.Add(options); + return request; + } + + public static RestClientRequest WithRequestOptions(this RestClientRequest request, Action options) + { + request.RequestBuilders.Add(options); + return request; + } + public static RestClientRequest WithResponseType(this RestClientRequest request) { request.ResponseContentType = typeof(T); diff --git a/ValheimServerGUI/Tools/LoggerExtensions.cs b/ValheimServerGUI.Tools/LoggerExtensions.cs similarity index 100% rename from ValheimServerGUI/Tools/LoggerExtensions.cs rename to ValheimServerGUI.Tools/LoggerExtensions.cs diff --git a/ValheimServerGUI/Tools/Logging/ApplicationLogger.cs b/ValheimServerGUI.Tools/Logging/ApplicationLogger.cs similarity index 100% rename from ValheimServerGUI/Tools/Logging/ApplicationLogger.cs rename to ValheimServerGUI.Tools/Logging/ApplicationLogger.cs diff --git a/ValheimServerGUI.Tools/Logging/ConcurrentBuffer.cs b/ValheimServerGUI.Tools/Logging/ConcurrentBuffer.cs new file mode 100644 index 0000000..54ea741 --- /dev/null +++ b/ValheimServerGUI.Tools/Logging/ConcurrentBuffer.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; + +namespace ValheimServerGUI.Tools.Logging +{ + public class ConcurrentBuffer : IEnumerable, IEnumerable, IReadOnlyCollection + { + public int BufferSize { get; } + + private ConcurrentQueue ConcurrentQueue = new(); + + public ConcurrentBuffer(int bufferSize) + { + if (bufferSize < 0) throw new ArgumentException("Buffer size must be >= 0"); + + this.BufferSize = bufferSize; + } + + public int Count => this.ConcurrentQueue.Count; + + public bool IsReadOnly => false; + + public void Enqueue(T item) + { + this.ConcurrentQueue.Enqueue(item); + + while (this.ConcurrentQueue.Count > this.BufferSize) + { + this.ConcurrentQueue.TryDequeue(out var _); + } + } + + public T Dequeue() + { + if (this.ConcurrentQueue.TryDequeue(out var item)) + { + return item; + } + + return default; + } + + public void Clear() + { + this.ConcurrentQueue.Clear(); + } + + public bool Contains(T item) + { + return this.ConcurrentQueue.Contains(item); + } + + public void CopyTo(T[] array, int arrayIndex) + { + this.ConcurrentQueue.CopyTo(array, arrayIndex); + } + + public IEnumerator GetEnumerator() + { + return this.ConcurrentQueue.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return this.ConcurrentQueue.GetEnumerator(); + } + } +} diff --git a/ValheimServerGUI/Tools/Logging/EventLogContext.cs b/ValheimServerGUI.Tools/Logging/EventLogContext.cs similarity index 100% rename from ValheimServerGUI/Tools/Logging/EventLogContext.cs rename to ValheimServerGUI.Tools/Logging/EventLogContext.cs diff --git a/ValheimServerGUI/Tools/Logging/EventLogger.cs b/ValheimServerGUI.Tools/Logging/EventLogger.cs similarity index 86% rename from ValheimServerGUI/Tools/Logging/EventLogger.cs rename to ValheimServerGUI.Tools/Logging/EventLogger.cs index 5adab5b..0de708b 100644 --- a/ValheimServerGUI/Tools/Logging/EventLogger.cs +++ b/ValheimServerGUI.Tools/Logging/EventLogger.cs @@ -1,10 +1,13 @@ using Microsoft.Extensions.Logging; using System; +using System.Collections.Generic; namespace ValheimServerGUI.Tools.Logging { public class EventLogger : IEventLogger { + private readonly ConcurrentBuffer ConcurrentBuffer = new(1000); + protected string CategoryName { get; set; } protected virtual bool FilterLog(EventLogContext context) @@ -19,6 +22,8 @@ protected virtual string FormatLog(EventLogContext context) #region IEventLogger implementation + public IEnumerable LogBuffer => this.ConcurrentBuffer; + public event EventHandler LogReceived; #endregion @@ -67,6 +72,9 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except return; } + var formattedMessage = $"[{context.Timestamp:G}] {context.Message}"; + this.ConcurrentBuffer.Enqueue(formattedMessage); + LogReceived?.Invoke(this, context); } diff --git a/ValheimServerGUI/Tools/Logging/IEventLogger.cs b/ValheimServerGUI.Tools/Logging/IEventLogger.cs similarity index 77% rename from ValheimServerGUI/Tools/Logging/IEventLogger.cs rename to ValheimServerGUI.Tools/Logging/IEventLogger.cs index 5827ea6..b315d30 100644 --- a/ValheimServerGUI/Tools/Logging/IEventLogger.cs +++ b/ValheimServerGUI.Tools/Logging/IEventLogger.cs @@ -1,10 +1,13 @@ using Microsoft.Extensions.Logging; using System; +using System.Collections.Generic; namespace ValheimServerGUI.Tools.Logging { public interface IEventLogger : ILogger { + IEnumerable LogBuffer { get; } + event EventHandler LogReceived; } diff --git a/ValheimServerGUI/Tools/ObjectExtensions.cs b/ValheimServerGUI.Tools/ObjectExtensions.cs similarity index 100% rename from ValheimServerGUI/Tools/ObjectExtensions.cs rename to ValheimServerGUI.Tools/ObjectExtensions.cs diff --git a/ValheimServerGUI/Tools/Processes/IProcessProvider.cs b/ValheimServerGUI.Tools/Processes/IProcessProvider.cs similarity index 100% rename from ValheimServerGUI/Tools/Processes/IProcessProvider.cs rename to ValheimServerGUI.Tools/Processes/IProcessProvider.cs diff --git a/ValheimServerGUI/Tools/Processes/ProcessExtensions.cs b/ValheimServerGUI.Tools/Processes/ProcessExtensions.cs similarity index 100% rename from ValheimServerGUI/Tools/Processes/ProcessExtensions.cs rename to ValheimServerGUI.Tools/Processes/ProcessExtensions.cs diff --git a/ValheimServerGUI/Tools/Processes/ProcessKeys.cs b/ValheimServerGUI.Tools/Processes/ProcessKeys.cs similarity index 100% rename from ValheimServerGUI/Tools/Processes/ProcessKeys.cs rename to ValheimServerGUI.Tools/Processes/ProcessKeys.cs diff --git a/ValheimServerGUI/Tools/Processes/ProcessProvider.cs b/ValheimServerGUI.Tools/Processes/ProcessProvider.cs similarity index 100% rename from ValheimServerGUI/Tools/Processes/ProcessProvider.cs rename to ValheimServerGUI.Tools/Processes/ProcessProvider.cs diff --git a/ValheimServerGUI.Tools/ValheimServerGUI.Tools.csproj b/ValheimServerGUI.Tools/ValheimServerGUI.Tools.csproj new file mode 100644 index 0000000..54d8c70 --- /dev/null +++ b/ValheimServerGUI.Tools/ValheimServerGUI.Tools.csproj @@ -0,0 +1,16 @@ + + + + net5.0 + + + true + ..\SolutionResources\ValheimServerGUI.snk + + + + + + + + diff --git a/ValheimServerGUI.sln b/ValheimServerGUI.sln index 1c8b095..0208450 100644 --- a/ValheimServerGUI.sln +++ b/ValheimServerGUI.sln @@ -7,7 +7,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ValheimServerGUI", "Valheim EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ValheimServerGUI.Controls", "ValheimServerGUI.Controls\ValheimServerGUI.Controls.csproj", "{36E75C0A-F596-4766-8148-D7C442501503}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ValheimServerGUI.Tests", "ValheimServerGUI.Tests\ValheimServerGUI.Tests.csproj", "{22F12ECA-242A-4D4D-9C2E-5949F353AD29}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ValheimServerGUI.Tests", "ValheimServerGUI.Tests\ValheimServerGUI.Tests.csproj", "{22F12ECA-242A-4D4D-9C2E-5949F353AD29}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ValheimServerGUI.Serverless", "ValheimServerGUI.Serverless\ValheimServerGUI.Serverless.csproj", "{14527A88-31E6-4FEB-9461-5291A08E8E7E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ValheimServerGUI.Serverless.Tests", "ValheimServerGUI.Serverless.Tests\ValheimServerGUI.Serverless.Tests.csproj", "{B366DF72-040D-464A-A200-E9CCD4F17B33}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ValheimServerGUI.Tools", "ValheimServerGUI.Tools\ValheimServerGUI.Tools.csproj", "{39BE3CA2-906A-446F-95DA-E37C359B6197}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -27,6 +33,18 @@ Global {22F12ECA-242A-4D4D-9C2E-5949F353AD29}.Debug|Any CPU.Build.0 = Debug|Any CPU {22F12ECA-242A-4D4D-9C2E-5949F353AD29}.Release|Any CPU.ActiveCfg = Release|Any CPU {22F12ECA-242A-4D4D-9C2E-5949F353AD29}.Release|Any CPU.Build.0 = Release|Any CPU + {14527A88-31E6-4FEB-9461-5291A08E8E7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {14527A88-31E6-4FEB-9461-5291A08E8E7E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {14527A88-31E6-4FEB-9461-5291A08E8E7E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {14527A88-31E6-4FEB-9461-5291A08E8E7E}.Release|Any CPU.Build.0 = Release|Any CPU + {B366DF72-040D-464A-A200-E9CCD4F17B33}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B366DF72-040D-464A-A200-E9CCD4F17B33}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B366DF72-040D-464A-A200-E9CCD4F17B33}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B366DF72-040D-464A-A200-E9CCD4F17B33}.Release|Any CPU.Build.0 = Release|Any CPU + {39BE3CA2-906A-446F-95DA-E37C359B6197}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {39BE3CA2-906A-446F-95DA-E37C359B6197}.Debug|Any CPU.Build.0 = Debug|Any CPU + {39BE3CA2-906A-446F-95DA-E37C359B6197}.Release|Any CPU.ActiveCfg = Release|Any CPU + {39BE3CA2-906A-446F-95DA-E37C359B6197}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/ValheimServerGUI/Forms/AboutForm.Designer.cs b/ValheimServerGUI/Forms/AboutForm.Designer.cs index b058330..ad234ac 100644 --- a/ValheimServerGUI/Forms/AboutForm.Designer.cs +++ b/ValheimServerGUI/Forms/AboutForm.Designer.cs @@ -156,7 +156,6 @@ private void InitializeComponent() this.Controls.Add(this.label1); this.Controls.Add(this.pictureBox1); this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle; - this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); this.MaximizeBox = false; this.MinimizeBox = false; this.Name = "AboutForm"; diff --git a/ValheimServerGUI/Forms/AboutForm.cs b/ValheimServerGUI/Forms/AboutForm.cs index 4328b7f..eac184b 100644 --- a/ValheimServerGUI/Forms/AboutForm.cs +++ b/ValheimServerGUI/Forms/AboutForm.cs @@ -10,6 +10,7 @@ public partial class AboutForm : Form public AboutForm() { InitializeComponent(); + this.AddApplicationIcon(); this.VersionLabel.Text = $"Version: {AssemblyHelper.GetApplicationVersion()}"; } diff --git a/ValheimServerGUI/Forms/AsyncPopout.Designer.cs b/ValheimServerGUI/Forms/AsyncPopout.Designer.cs new file mode 100644 index 0000000..eb81c64 --- /dev/null +++ b/ValheimServerGUI/Forms/AsyncPopout.Designer.cs @@ -0,0 +1,101 @@ + +namespace ValheimServerGUI.Forms +{ + partial class AsyncPopout + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.components = new System.ComponentModel.Container(); + this.LoadingLabel = new System.Windows.Forms.Label(); + this.CloseButton = new System.Windows.Forms.Button(); + this.ProgressBar = new System.Windows.Forms.ProgressBar(); + this.Timer = new System.Windows.Forms.Timer(this.components); + this.SuspendLayout(); + // + // LoadingLabel + // + this.LoadingLabel.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.LoadingLabel.Location = new System.Drawing.Point(12, 9); + this.LoadingLabel.Name = "LoadingLabel"; + this.LoadingLabel.Size = new System.Drawing.Size(260, 64); + this.LoadingLabel.TabIndex = 0; + this.LoadingLabel.Text = "Loading..."; + this.LoadingLabel.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; + // + // CloseButton + // + this.CloseButton.Location = new System.Drawing.Point(197, 76); + this.CloseButton.Name = "CloseButton"; + this.CloseButton.Size = new System.Drawing.Size(75, 23); + this.CloseButton.TabIndex = 1; + this.CloseButton.Text = "Cancel"; + this.CloseButton.UseVisualStyleBackColor = true; + // + // ProgressBar + // + this.ProgressBar.Location = new System.Drawing.Point(13, 76); + this.ProgressBar.MarqueeAnimationSpeed = 16; + this.ProgressBar.Name = "ProgressBar"; + this.ProgressBar.Size = new System.Drawing.Size(178, 23); + this.ProgressBar.Step = 2; + this.ProgressBar.Style = System.Windows.Forms.ProgressBarStyle.Marquee; + this.ProgressBar.TabIndex = 2; + // + // Timer + // + this.Timer.Enabled = true; + // + // AsyncPopout + // + this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(284, 111); + this.ControlBox = false; + this.Controls.Add(this.ProgressBar); + this.Controls.Add(this.CloseButton); + this.Controls.Add(this.LoadingLabel); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle; + this.Name = "AsyncPopout"; + this.ShowInTaskbar = false; + this.SizeGripStyle = System.Windows.Forms.SizeGripStyle.Hide; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; + this.Text = "Loading..."; + this.TopMost = true; + this.ResumeLayout(false); + + } + + #endregion + + private System.Windows.Forms.Label LoadingLabel; + private System.Windows.Forms.Button CloseButton; + private System.Windows.Forms.ProgressBar ProgressBar; + private System.Windows.Forms.Timer Timer; + } +} \ No newline at end of file diff --git a/ValheimServerGUI/Forms/AsyncPopout.cs b/ValheimServerGUI/Forms/AsyncPopout.cs new file mode 100644 index 0000000..e3794de --- /dev/null +++ b/ValheimServerGUI/Forms/AsyncPopout.cs @@ -0,0 +1,113 @@ +using System; +using System.Threading.Tasks; +using System.Windows.Forms; +using ValheimServerGUI.Tools; + +namespace ValheimServerGUI.Forms +{ + public partial class AsyncPopout : Form + { + protected Task Task; + + protected AsyncPopoutOptions Options; + + private event EventHandler TaskFinished; + + private bool AutoCloseOnFinished; + + private string FinishedMessage; + + private AsyncPopout() + { + InitializeComponent(); + this.AddApplicationIcon(); + + this.CloseButton.Click += this.BuildEventHandler(this.Close); + this.TaskFinished += this.BuildEventHandler(this.OnTaskFinished); + } + + public AsyncPopout(Task task, Action optionsBuilder = null) + : this() + { + if (task == null) throw new ArgumentException("Task cannot be null", nameof(task)); + + var options = new AsyncPopoutOptions(); + optionsBuilder?.Invoke(options); + this.Options = options; + + this.Task = task; + this.Text = options.Title ?? this.Text; + this.LoadingLabel.Text = options.Text ?? this.LoadingLabel.Text; + } + + protected override void OnLoad(EventArgs e) + { + base.OnLoad(e); + + this.Task = this.Task.ContinueWith(this.TaskContinuationHandler); + } + + protected void OnTaskFinished() + { + if (this.AutoCloseOnFinished) + { + this.Close(); + return; + } + + this.LoadingLabel.Text = this.FinishedMessage ?? "Task Complete!"; + this.CloseButton.Text = "Close"; + this.ProgressBar.Visible = false; + } + + private Task TaskContinuationHandler(Task task) + { + if (task.Status == TaskStatus.RanToCompletion) + { + if (this.Options.CloseOnSuccess) return this.CloseAsync(); + + this.FinishedMessage = this.Options.SuccessMessage; + } + else if (task.Exception != null) + { + if (this.Options.CloseOnFailure) return this.CloseAsync(); + + var errMessage = this.Options.FailureMessage ?? "The task was cancelled due to an error."; + var ex = task.Exception.GetPrimaryException(); + this.FinishedMessage = $"{errMessage}\r\n{ex.GetType().Name}\r\n{ex.Message}"; + } + else + { + if (this.Options.CloseOnFailure) return this.CloseAsync(); + + this.FinishedMessage = this.Options.FailureMessage ?? "The task was cancelled due to an unknown error."; + } + + this.TaskFinished?.Invoke(this, EventArgs.Empty); + + return Task.CompletedTask; + } + + private Task CloseAsync() + { + this.AutoCloseOnFinished = true; + this.TaskFinished?.Invoke(this, EventArgs.Empty); + return Task.CompletedTask; + } + + public class AsyncPopoutOptions + { + public string Title { get; set; } + + public string Text { get; set; } + + public bool CloseOnSuccess { get; set; } + + public string SuccessMessage { get; set; } + + public bool CloseOnFailure { get; set; } + + public string FailureMessage { get; set; } + } + } +} diff --git a/ValheimServerGUI/Forms/AsyncPopout.resx b/ValheimServerGUI/Forms/AsyncPopout.resx new file mode 100644 index 0000000..f298a7b --- /dev/null +++ b/ValheimServerGUI/Forms/AsyncPopout.resx @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/ValheimServerGUI/Forms/BugReportForm.Designer.cs b/ValheimServerGUI/Forms/BugReportForm.Designer.cs new file mode 100644 index 0000000..d2d77c2 --- /dev/null +++ b/ValheimServerGUI/Forms/BugReportForm.Designer.cs @@ -0,0 +1,117 @@ + +namespace ValheimServerGUI.Forms +{ + partial class BugReportForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(BugReportForm)); + this.ButtonCancel = new System.Windows.Forms.Button(); + this.ButtonSubmit = new System.Windows.Forms.Button(); + this.ContactInfoField = new ValheimServerGUI.Forms.Controls.TextFormField(); + this.BugReportField = new ValheimServerGUI.Forms.Controls.TextFormField(); + this.SuspendLayout(); + // + // ButtonCancel + // + this.ButtonCancel.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this.ButtonCancel.Location = new System.Drawing.Point(272, 182); + this.ButtonCancel.Name = "ButtonCancel"; + this.ButtonCancel.Size = new System.Drawing.Size(75, 23); + this.ButtonCancel.TabIndex = 0; + this.ButtonCancel.Text = "Cancel"; + this.ButtonCancel.UseVisualStyleBackColor = true; + // + // ButtonSubmit + // + this.ButtonSubmit.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this.ButtonSubmit.Enabled = false; + this.ButtonSubmit.Location = new System.Drawing.Point(191, 182); + this.ButtonSubmit.Name = "ButtonSubmit"; + this.ButtonSubmit.Size = new System.Drawing.Size(75, 23); + this.ButtonSubmit.TabIndex = 1; + this.ButtonSubmit.Text = "Submit"; + this.ButtonSubmit.UseVisualStyleBackColor = true; + // + // ContactInfoField + // + this.ContactInfoField.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.ContactInfoField.HelpText = resources.GetString("ContactInfoField.HelpText"); + this.ContactInfoField.HideValue = false; + this.ContactInfoField.LabelText = "(Optional) Contact details for follow-up"; + this.ContactInfoField.Location = new System.Drawing.Point(12, 135); + this.ContactInfoField.MaxLength = 255; + this.ContactInfoField.Multiline = false; + this.ContactInfoField.Name = "ContactInfoField"; + this.ContactInfoField.Size = new System.Drawing.Size(334, 41); + this.ContactInfoField.TabIndex = 2; + this.ContactInfoField.Value = ""; + // + // BugReportField + // + this.BugReportField.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.BugReportField.HelpText = resources.GetString("BugReportField.HelpText"); + this.BugReportField.HideValue = false; + this.BugReportField.LabelText = "What\'s the problem? And how did you encounter it?"; + this.BugReportField.Location = new System.Drawing.Point(12, 12); + this.BugReportField.MaxLength = 2000; + this.BugReportField.Multiline = true; + this.BugReportField.Name = "BugReportField"; + this.BugReportField.Size = new System.Drawing.Size(334, 117); + this.BugReportField.TabIndex = 4; + this.BugReportField.Value = ""; + // + // BugReportForm + // + this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(359, 217); + this.Controls.Add(this.BugReportField); + this.Controls.Add(this.ContactInfoField); + this.Controls.Add(this.ButtonSubmit); + this.Controls.Add(this.ButtonCancel); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle; + this.MaximizeBox = false; + this.MinimizeBox = false; + this.Name = "BugReportForm"; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; + this.Text = "Submit a Bug Report"; + this.ResumeLayout(false); + + } + + #endregion + + private System.Windows.Forms.Button ButtonCancel; + private System.Windows.Forms.Button ButtonSubmit; + private Controls.TextFormField ContactInfoField; + private Controls.TextFormField BugReportField; + } +} \ No newline at end of file diff --git a/ValheimServerGUI/Forms/BugReportForm.cs b/ValheimServerGUI/Forms/BugReportForm.cs new file mode 100644 index 0000000..fd47ba6 --- /dev/null +++ b/ValheimServerGUI/Forms/BugReportForm.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Windows.Forms; +using ValheimServerGUI.Tools; +using ValheimServerGUI.Tools.Logging; + +namespace ValheimServerGUI.Forms +{ + public partial class BugReportForm : Form + { + private readonly IRuneberryApiClient RuneberryApiClient; + + private readonly IEventLogger Logger; + + public BugReportForm( + IRuneberryApiClient runeberryApiClient, + IEventLogger logger) + { + this.RuneberryApiClient = runeberryApiClient; + this.Logger = logger; + + InitializeComponent(); + this.AddApplicationIcon(); + + this.ButtonSubmit.Click += this.BuildEventHandler(this.ButtonSubmit_Click); + this.ButtonCancel.Click += this.BuildEventHandler(this.ButtonCancel_Click); + this.BugReportField.ValueChanged += this.BuildEventHandler(this.BugReportField_ValueChanged); + } + + protected override void OnShown(EventArgs e) + { + base.OnShown(e); + + this.ClearForm(); + } + + protected override void OnClosed(EventArgs e) + { + base.OnClosed(e); + + this.ClearForm(); + } + + private void ButtonCancel_Click() + { + this.Close(); + } + + private void ButtonSubmit_Click() + { + this.SubmitBugReport(); + } + + private void BugReportField_ValueChanged(string value) + { + // Only enable the Submit button when there is some content in the bug report + this.ButtonSubmit.Enabled = !string.IsNullOrWhiteSpace(value); + } + + private void ClearForm() + { + this.BugReportField.Value = string.Empty; + this.ContactInfoField.Value = string.Empty; + } + + private void SubmitBugReport() + { + var crashReport = AssemblyHelper.BuildCrashReport(); + + var additionalInfo = new Dictionary + { + { "BugReport", this.BugReportField.Value }, + { "ContactInfo", this.ContactInfoField.Value }, + }; + + crashReport.Source = "BugReport"; + crashReport.AdditionalInfo = additionalInfo; + crashReport.Logs = this.Logger.LogBuffer.Reverse().Take(100).ToList(); + + var task = RuneberryApiClient.SendCrashReportAsync(crashReport); + var asyncPopout = new AsyncPopout(task, o => + { + o.Title = "Bug Report"; + o.Text = "Submitting bug report..."; + o.SuccessMessage = "Bug report submitted. Thank you!"; + o.FailureMessage = "Failed to submit bug report.\r\nContact Runeberry Software for further support."; + }); + + asyncPopout.ShowDialog(); + + this.Close(); + } + } +} diff --git a/ValheimServerGUI/Forms/BugReportForm.resx b/ValheimServerGUI/Forms/BugReportForm.resx new file mode 100644 index 0000000..2970a7a --- /dev/null +++ b/ValheimServerGUI/Forms/BugReportForm.resx @@ -0,0 +1,290 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + If you'd like, supply your email address, Twitter handle, +or any other way we can contact you in case we need +more information to follow up on your bug report. + +Of course, we will never share this information with any +third parties. (I don't even know how to do that) + + + Describe the bug you're experiencing in detail, including: + - A detailed description of the problem you're experiencing + - Any steps required to reproduce the bug + +Your bug report will be submitted alongside some details +about your computer, which version you're running, etc. +to help us further troubleshoot the problem. No personal +or identifying details about your computer will be collected. + + + + + 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 + 7f8DbWhZKaNP5FoAAAAASUVORK5CYII= + + + \ No newline at end of file diff --git a/ValheimServerGUI/Forms/DirectoriesForm.Designer.cs b/ValheimServerGUI/Forms/DirectoriesForm.Designer.cs index 088bb63..d6012a9 100644 --- a/ValheimServerGUI/Forms/DirectoriesForm.Designer.cs +++ b/ValheimServerGUI/Forms/DirectoriesForm.Designer.cs @@ -126,7 +126,6 @@ private void InitializeComponent() this.Controls.Add(this.ButtonOK); this.Controls.Add(this.ButtonCancel); this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle; - this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); this.MaximizeBox = false; this.MinimizeBox = false; this.Name = "DirectoriesForm"; diff --git a/ValheimServerGUI/Forms/DirectoriesForm.cs b/ValheimServerGUI/Forms/DirectoriesForm.cs index 4599a39..1c4d9ce 100644 --- a/ValheimServerGUI/Forms/DirectoriesForm.cs +++ b/ValheimServerGUI/Forms/DirectoriesForm.cs @@ -1,6 +1,7 @@ using System; using System.Windows.Forms; using ValheimServerGUI.Game; +using ValheimServerGUI.Tools; namespace ValheimServerGUI.Forms { @@ -13,6 +14,7 @@ public partial class DirectoriesForm : Form public DirectoriesForm() { InitializeComponent(); + this.AddApplicationIcon(); } public DirectoriesForm(IUserPreferencesProvider userPrefsProvider, IValheimFileProvider fileProvider) : this() diff --git a/ValheimServerGUI/Forms/MainWindow.Designer.cs b/ValheimServerGUI/Forms/MainWindow.Designer.cs index 73ecd2f..ef40276 100644 --- a/ValheimServerGUI/Forms/MainWindow.Designer.cs +++ b/ValheimServerGUI/Forms/MainWindow.Designer.cs @@ -40,6 +40,7 @@ private void InitializeComponent() this.MenuItemHelp = new System.Windows.Forms.ToolStripMenuItem(); this.MenuItemHelpManual = new System.Windows.Forms.ToolStripMenuItem(); this.MenuItemHelpPortForwarding = new System.Windows.Forms.ToolStripMenuItem(); + this.MenuItemHelpBugReport = new System.Windows.Forms.ToolStripMenuItem(); this.MenuItemHelpSeparator1 = new System.Windows.Forms.ToolStripSeparator(); this.MenuItemHelpUpdates = new System.Windows.Forms.ToolStripMenuItem(); this.MenuItemHelpAbout = new System.Windows.Forms.ToolStripMenuItem(); @@ -162,6 +163,7 @@ private void InitializeComponent() this.MenuItemHelp.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { this.MenuItemHelpManual, this.MenuItemHelpPortForwarding, + this.MenuItemHelpBugReport, this.MenuItemHelpSeparator1, this.MenuItemHelpUpdates, this.MenuItemHelpAbout}); @@ -173,32 +175,39 @@ private void InitializeComponent() // this.MenuItemHelpManual.Image = ((System.Drawing.Image)(resources.GetObject("MenuItemHelpManual.Image"))); this.MenuItemHelpManual.Name = "MenuItemHelpManual"; - this.MenuItemHelpManual.Size = new System.Drawing.Size(171, 22); + this.MenuItemHelpManual.Size = new System.Drawing.Size(192, 22); this.MenuItemHelpManual.Text = "Online &Manual"; // // MenuItemHelpPortForwarding // this.MenuItemHelpPortForwarding.Image = global::ValheimServerGUI.Properties.Resources.OpenWeb_16x; this.MenuItemHelpPortForwarding.Name = "MenuItemHelpPortForwarding"; - this.MenuItemHelpPortForwarding.Size = new System.Drawing.Size(171, 22); + this.MenuItemHelpPortForwarding.Size = new System.Drawing.Size(192, 22); this.MenuItemHelpPortForwarding.Text = "&Port Forwarding"; // + // MenuItemHelpBugReport + // + this.MenuItemHelpBugReport.Image = global::ValheimServerGUI.Properties.Resources.NewBug_16x; + this.MenuItemHelpBugReport.Name = "MenuItemHelpBugReport"; + this.MenuItemHelpBugReport.Size = new System.Drawing.Size(192, 22); + this.MenuItemHelpBugReport.Text = "Submit a &Bug Report..."; + // // MenuItemHelpSeparator1 // this.MenuItemHelpSeparator1.Name = "MenuItemHelpSeparator1"; - this.MenuItemHelpSeparator1.Size = new System.Drawing.Size(168, 6); + this.MenuItemHelpSeparator1.Size = new System.Drawing.Size(189, 6); // // MenuItemHelpUpdates // this.MenuItemHelpUpdates.Image = global::ValheimServerGUI.Properties.Resources.UnsyncedCommits_16x_Horiz; this.MenuItemHelpUpdates.Name = "MenuItemHelpUpdates"; - this.MenuItemHelpUpdates.Size = new System.Drawing.Size(171, 22); + this.MenuItemHelpUpdates.Size = new System.Drawing.Size(192, 22); this.MenuItemHelpUpdates.Text = "Check for &Updates"; // // MenuItemHelpAbout // this.MenuItemHelpAbout.Name = "MenuItemHelpAbout"; - this.MenuItemHelpAbout.Size = new System.Drawing.Size(171, 22); + this.MenuItemHelpAbout.Size = new System.Drawing.Size(192, 22); this.MenuItemHelpAbout.Text = "&About..."; // // StatusStrip @@ -282,6 +291,7 @@ private void InitializeComponent() this.WorldSelectNewNameField.LabelText = "New World Name"; this.WorldSelectNewNameField.Location = new System.Drawing.Point(6, 45); this.WorldSelectNewNameField.MaxLength = 20; + this.WorldSelectNewNameField.Multiline = false; this.WorldSelectNewNameField.Name = "WorldSelectNewNameField"; this.WorldSelectNewNameField.Size = new System.Drawing.Size(234, 41); this.WorldSelectNewNameField.TabIndex = 18; @@ -379,6 +389,7 @@ private void InitializeComponent() this.ServerPasswordField.LabelText = "Server Password"; this.ServerPasswordField.Location = new System.Drawing.Point(0, 47); this.ServerPasswordField.MaxLength = 64; + this.ServerPasswordField.Multiline = false; this.ServerPasswordField.Name = "ServerPasswordField"; this.ServerPasswordField.Size = new System.Drawing.Size(243, 41); this.ServerPasswordField.TabIndex = 11; @@ -391,6 +402,7 @@ private void InitializeComponent() this.ServerNameField.LabelText = "Server Name"; this.ServerNameField.Location = new System.Drawing.Point(0, 0); this.ServerNameField.MaxLength = 64; + this.ServerNameField.Multiline = false; this.ServerNameField.Name = "ServerNameField"; this.ServerNameField.Size = new System.Drawing.Size(243, 41); this.ServerNameField.TabIndex = 10; @@ -775,7 +787,6 @@ private void InitializeComponent() this.Controls.Add(this.Tabs); this.Controls.Add(this.StatusStrip); this.Controls.Add(this.MenuStrip); - this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); this.MainMenuStrip = this.MenuStrip; this.MaximizeBox = false; this.MinimumSize = new System.Drawing.Size(500, 371); @@ -868,5 +879,6 @@ private void InitializeComponent() private System.Windows.Forms.ToolStripStatusLabel StatusStripLabelRight; private System.Windows.Forms.Timer UpdateCheckTimer; private System.Windows.Forms.ToolStripMenuItem MenuItemFilePreferences; + private System.Windows.Forms.ToolStripMenuItem MenuItemHelpBugReport; } } \ No newline at end of file diff --git a/ValheimServerGUI/Forms/MainWindow.cs b/ValheimServerGUI/Forms/MainWindow.cs index f40ef03..081d832 100644 --- a/ValheimServerGUI/Forms/MainWindow.cs +++ b/ValheimServerGUI/Forms/MainWindow.cs @@ -17,6 +17,11 @@ namespace ValheimServerGUI.Forms { public partial class MainWindow : Form { +#if DEBUG + private static readonly bool SimulateConstructorException = false; + private static readonly bool SimulateStartServerException = false; + private static readonly bool SimulateStopServerException = false; +#endif private static readonly string NL = Environment.NewLine; private const string LogViewServer = "Server"; private const string LogViewApplication = "Application"; @@ -30,10 +35,7 @@ public partial class MainWindow : Form { ServerStatus.Starting, Resources.UnsyncedCommits_16x_Horiz }, { ServerStatus.Running, Resources.StatusRun_16x }, { ServerStatus.Stopping, Resources.UnsyncedCommits_16x_Horiz }, - }; - - private readonly TimeSpan UpdateCheckInterval = TimeSpan.Parse(Resources.UpdateCheckInterval); - private DateTime NextUpdateCheck = DateTime.MaxValue; + }; private readonly IFormProvider FormProvider; private readonly IUserPreferencesProvider UserPrefsProvider; @@ -43,7 +45,7 @@ public partial class MainWindow : Form private readonly ValheimServerLogger ServerLogger; private readonly IEventLogger Logger; private readonly IIpAddressProvider IpAddressProvider; - private readonly IGitHubClient GitHubClient; + private readonly ISoftwareUpdateProvider SoftwareUpdateProvider; public MainWindow( IFormProvider formProvider, @@ -54,8 +56,11 @@ public MainWindow( ValheimServerLogger serverLogger, IEventLogger appLogger, IIpAddressProvider ipAddressProvider, - IGitHubClient gitHubClient) + ISoftwareUpdateProvider softwareUpdateProvider) { +#if DEBUG + if (SimulateConstructorException) throw new InvalidOperationException("Intentional exception thrown for testing"); +#endif this.FormProvider = formProvider; this.UserPrefsProvider = userPrefsProvider; this.FileProvider = fileProvider; @@ -64,9 +69,10 @@ public MainWindow( this.ServerLogger = serverLogger; this.Logger = appLogger; this.IpAddressProvider = ipAddressProvider; - this.GitHubClient = gitHubClient; + this.SoftwareUpdateProvider = softwareUpdateProvider; InitializeComponent(); // WinForms generated code, always first + this.AddApplicationIcon(); InitializeImages(); InitializeServer(); InitializeFormEvents(); @@ -94,12 +100,15 @@ private void InitializeServer() this.IpAddressProvider.ExternalIpReceived += this.BuildEventHandler(this.IpAddressProvider_ExternalIpReceived); this.IpAddressProvider.InternalIpReceived += this.BuildEventHandler(this.IpAddressProvider_InternalIpReceived); + + this.SoftwareUpdateProvider.UpdateCheckStarted += this.BuildEventHandler(this.SoftwareUpdateProvider_UpdateCheckStarted); + this.SoftwareUpdateProvider.UpdateCheckFinished += this.BuildEventHandler(this.SoftwareUpdateProvider_UpdateCheckFinished); } private void InitializeFormEvents() { // MainWindow - this.Shown += this.BuildEventHandlerAsync(this.MainWindow_Load, 250); + this.Shown += this.BuildEventHandler(this.MainWindow_Load); // Menu items this.MenuItemFilePreferences.Click += this.MenuItemFilePreferences_Click; @@ -107,7 +116,8 @@ private void InitializeFormEvents() this.MenuItemFileClose.Click += this.MenuItemFileClose_Clicked; this.MenuItemHelpManual.Click += this.MenuItemHelpManual_Click; this.MenuItemHelpPortForwarding.Click += this.MenuItemHelpPortForwarding_Clicked; - this.MenuItemHelpUpdates.Click += this.BuildEventHandlerAsync(this.MenuItemHelpUpdates_Clicked); + this.MenuItemHelpBugReport.Click += this.MenuItemHelpBugReport_Click; + this.MenuItemHelpUpdates.Click += this.BuildEventHandler(this.MenuItemHelpUpdates_Clicked); this.MenuItemHelpAbout.Click += this.MenuItemHelpAbout_Clicked; // Tray icon @@ -119,7 +129,7 @@ private void InitializeFormEvents() // Timers this.ServerRefreshTimer.Tick += this.ServerRefreshTimer_Tick; - this.UpdateCheckTimer.Tick += this.BuildEventHandlerAsync(this.UpdateCheckTimer_Tick); + this.UpdateCheckTimer.Tick += this.BuildEventHandler(this.UpdateCheckTimer_Tick); // Tabs this.TabPlayers.VisibleChanged += this.TabPlayers_VisibleChanged; @@ -136,7 +146,7 @@ private void InitializeFormEvents() this.CopyButtonExternalIpAddress.CopyFunction = () => this.LabelExternalIpAddress.Value; this.CopyButtonInternalIpAddress.CopyFunction = () => this.LabelInternalIpAddress.Value; this.CopyButtonLocalIpAddress.CopyFunction = () => this.LabelLocalIpAddress.Value; - this.StatusStripLabelRight.Click += this.BuildEventHandlerAsync(this.StatusStripLabelRight_Click); + this.StatusStripLabelRight.Click += this.BuildEventHandler(this.StatusStripLabelRight_Click); // Form fields this.ShowPasswordField.ValueChanged += this.ShowPasswordField_Changed; @@ -167,15 +177,9 @@ private void InitializeFormFields() #region MainWindow Events - private Task MainWindow_Load() + private void MainWindow_Load() { this.Logger.LogInformation($"Valheim Server GUI v{AssemblyHelper.GetApplicationVersion()} - Loaded OK"); - - return Task.WhenAll( - this.RefreshInternalIpAsync(), - this.RefreshExternalIpAsync(), - this.CheckForUpdatesAsync(false) - ); } protected override void OnShown(EventArgs e) @@ -288,9 +292,15 @@ private void MenuItemHelpPortForwarding_Clicked(object sender, EventArgs e) WebHelper.OpenWebAddress(Resources.UrlPortForwardingGuide); } - private async Task MenuItemHelpUpdates_Clicked() + private void MenuItemHelpBugReport_Click(object sender, EventArgs e) { - await this.CheckForUpdatesAsync(true); + var bugReportForm = FormProvider.GetForm(); + bugReportForm.ShowDialog(); + } + + private void MenuItemHelpUpdates_Clicked() + { + this.CheckForUpdates(true); } private void MenuItemHelpAbout_Clicked(object sender, EventArgs e) @@ -305,6 +315,9 @@ private void MenuItemHelpAbout_Clicked(object sender, EventArgs e) private void ButtonStopServer_Click(object sender, EventArgs e) { +#if DEBUG + if (SimulateStopServerException) throw new InvalidOperationException("Intentional exception thrown for testing"); +#endif Server.Stop(); } @@ -429,12 +442,9 @@ private void ServerRefreshTimer_Tick(object sender, EventArgs e) if (this.TabServerDetails.Visible) this.RefreshServerDetails(); } - private async Task UpdateCheckTimer_Tick() + private void UpdateCheckTimer_Tick() { - if (DateTime.UtcNow > this.NextUpdateCheck) - { - await this.CheckForUpdatesAsync(false); - } + this.CheckForUpdates(false); } private void PlayersTable_SelectionChanged(object sender, EventArgs e) @@ -444,16 +454,16 @@ private void PlayersTable_SelectionChanged(object sender, EventArgs e) this.ButtonRemovePlayer.Enabled = isSelected && row.Entity.PlayerStatus == PlayerStatus.Offline; } - private async Task StatusStripLabelRight_Click() + private void StatusStripLabelRight_Click() { if (!this.StatusStripLabelRight.IsLink) return; - await this.CheckForUpdatesAsync(true); + this.CheckForUpdates(true); } #endregion - #region Server Events + #region Service Events private void OnApplicationLogReceived(EventLogContext logEvent) { @@ -525,6 +535,73 @@ private void IpAddressProvider_InternalIpReceived(string ip) this.RefreshIpPorts(); } + private void SoftwareUpdateProvider_UpdateCheckStarted() + { + this.SetStatusTextRight("Checking for updates...", Resources.Loading_Blue_16x, false); + } + + private void SoftwareUpdateProvider_UpdateCheckFinished(SoftwareUpdateEventArgs e) + { + if (!e.IsSuccessful) + { + this.SetStatusTextRight($"Update check failed", Resources.StatusCriticalError_16x, true); + + if (e.IsManualCheck) + { + var exception = e.Exception.GetPrimaryException(); + var result = MessageBox.Show( + $"Update check failed: {exception.Message}" + Environment.NewLine + + "Would you like to go to the download page?", + "Check for Updates", + MessageBoxButtons.YesNo, + MessageBoxIcon.Error); + + if (result == DialogResult.Yes) + { + WebHelper.OpenWebAddress(Resources.UrlUpdates); + } + } + } + else if (e.IsNewerVersionAvailable) + { + this.SetStatusTextRight($"Update available ({e.LatestVersion})", Resources.StatusWarning_16x, true); + + if (e.IsManualCheck) + { + var result = MessageBox.Show( + $"A newer version of ValheimServerGUI is available." + Environment.NewLine + + "Would you like to go to the download page?", + "Check for Updates", + MessageBoxButtons.YesNo, + MessageBoxIcon.Warning); + + if (result == DialogResult.Yes) + { + WebHelper.OpenWebAddress(Resources.UrlUpdates); + } + } + } + else + { + this.SetStatusTextRight($"Up to date ({e.LatestVersion})", Resources.StatusOK_16x, false); + + if (e.IsManualCheck) + { + var result = MessageBox.Show( + "You are running the latest version of ValheimServerGUI." + Environment.NewLine + + "Would you like to go to the download page?", + "Check for Updates", + MessageBoxButtons.YesNo, + MessageBoxIcon.Question); + + if (result == DialogResult.Yes) + { + WebHelper.OpenWebAddress(Resources.UrlUpdates); + } + } + } + } + #endregion #region Common Methods @@ -550,6 +627,9 @@ private void RunStartupStuff() private void StartServer() { +#if DEBUG + if (SimulateStartServerException) throw new InvalidOperationException("Intentional exception thrown for testing"); +#endif string worldName; bool newWorld = this.WorldSelectRadioNew.Value; @@ -708,16 +788,6 @@ private void RefreshServerDetails() } } - private async Task RefreshExternalIpAsync() - { - if (this.LabelExternalIpAddress.Value == IpLoadingText) await this.IpAddressProvider.GetExternalIpAddressAsync(); - } - - private async Task RefreshInternalIpAsync() - { - if (this.LabelInternalIpAddress.Value == IpLoadingText) await this.IpAddressProvider.GetInternalIpAddressAsync(); - } - private void RefreshIpPorts() { const string ipExpr = @"^([\d]{1,3}\.[\d]{1,3}\.[\d]{1,3}\.[\d]{1,3})"; @@ -830,60 +900,9 @@ private void LoadFormValuesFromUserPrefs(UserPreferences prefs) this.WorldSelectRadioExisting.Value = true; } - private async Task CheckForUpdatesAsync(bool isManualCheck) + private void CheckForUpdates(bool isManualCheck) { - if (!isManualCheck) - { - this.NextUpdateCheck = DateTime.UtcNow + this.UpdateCheckInterval; - - var prefs = this.UserPrefsProvider.LoadPreferences(); - if (!prefs.CheckForUpdates) return; - } - - this.SetStatusTextRight("Checking for updates...", Resources.Loading_Blue_16x, false); - - var currentVersion = AssemblyHelper.GetApplicationVersion(); - var release = await this.GitHubClient.GetLatestReleaseAsync(); - - if (AssemblyHelper.IsNewerVersion(release?.TagName)) - { - this.SetStatusTextRight($"Update available ({release.TagName})", Resources.StatusWarning_16x, true); - - if (isManualCheck) - { - var result = MessageBox.Show( - $"A newer version of ValheimServerGUI is available." + Environment.NewLine + - "Would you like to go to the download page?", - "Check for Updates", - MessageBoxButtons.YesNo, - MessageBoxIcon.Warning); - - if (result == DialogResult.Yes) - { - WebHelper.OpenWebAddress(Resources.UrlUpdates); - } - } - } - else - { - currentVersion = release.TagName ?? currentVersion; // Use the v-prefixed version if available - this.SetStatusTextRight($"Up to date ({currentVersion})", Resources.StatusOK_16x, false); - - if (isManualCheck) - { - var result = MessageBox.Show( - "You are running the latest version of ValheimServerGUI." + Environment.NewLine + - "Would you like to go to the download page?", - "Check for Updates", - MessageBoxButtons.YesNo, - MessageBoxIcon.Question); - - if (result == DialogResult.Yes) - { - WebHelper.OpenWebAddress(Resources.UrlUpdates); - } - } - } + Task.Run(() => this.SoftwareUpdateProvider.CheckForUpdatesAsync(isManualCheck)); } private void CloseApplicationOnServerStopped() diff --git a/ValheimServerGUI/Forms/PlayerDetailsForm.Designer.cs b/ValheimServerGUI/Forms/PlayerDetailsForm.Designer.cs index 313ca3e..e54cd13 100644 --- a/ValheimServerGUI/Forms/PlayerDetailsForm.Designer.cs +++ b/ValheimServerGUI/Forms/PlayerDetailsForm.Designer.cs @@ -161,7 +161,6 @@ private void InitializeComponent() this.Controls.Add(this.ButtonOK); this.Controls.Add(this.PlayerNameField); this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog; - this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); this.MaximizeBox = false; this.MinimizeBox = false; this.Name = "PlayerDetailsForm"; diff --git a/ValheimServerGUI/Forms/PlayerDetailsForm.cs b/ValheimServerGUI/Forms/PlayerDetailsForm.cs index e1c69a8..844936f 100644 --- a/ValheimServerGUI/Forms/PlayerDetailsForm.cs +++ b/ValheimServerGUI/Forms/PlayerDetailsForm.cs @@ -16,6 +16,7 @@ public PlayerDetailsForm(IPlayerDataRepository playerDataProvider) this.PlayerDataProvider = playerDataProvider; InitializeComponent(); + this.AddApplicationIcon(); this.ButtonRefresh.Click += ButtonRefresh_Click; this.ButtonOK.Click += ButtonOK_Click; diff --git a/ValheimServerGUI/Forms/PreferencesForm.Designer.cs b/ValheimServerGUI/Forms/PreferencesForm.Designer.cs index f0bd746..b815fa4 100644 --- a/ValheimServerGUI/Forms/PreferencesForm.Designer.cs +++ b/ValheimServerGUI/Forms/PreferencesForm.Designer.cs @@ -125,7 +125,6 @@ private void InitializeComponent() this.Controls.Add(this.ButtonOK); this.Controls.Add(this.ButtonCancel); this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle; - this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); this.MaximizeBox = false; this.MinimizeBox = false; this.Name = "PreferencesForm"; diff --git a/ValheimServerGUI/Forms/PreferencesForm.cs b/ValheimServerGUI/Forms/PreferencesForm.cs index 84765d2..f6e9fa3 100644 --- a/ValheimServerGUI/Forms/PreferencesForm.cs +++ b/ValheimServerGUI/Forms/PreferencesForm.cs @@ -15,6 +15,7 @@ public partial class PreferencesForm : Form public PreferencesForm() { InitializeComponent(); + this.AddApplicationIcon(); } public PreferencesForm(IUserPreferencesProvider userPrefsProvider, ILogger logger) : this() diff --git a/ValheimServerGUI/Forms/SplashForm.Designer.cs b/ValheimServerGUI/Forms/SplashForm.Designer.cs new file mode 100644 index 0000000..5302c01 --- /dev/null +++ b/ValheimServerGUI/Forms/SplashForm.Designer.cs @@ -0,0 +1,78 @@ + +namespace ValheimServerGUI.Forms +{ + partial class SplashForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.AppNameLabel = new System.Windows.Forms.Label(); + this.ProgressBar = new System.Windows.Forms.ProgressBar(); + this.SuspendLayout(); + // + // AppNameLabel + // + this.AppNameLabel.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.AppNameLabel.Location = new System.Drawing.Point(12, 9); + this.AppNameLabel.Name = "AppNameLabel"; + this.AppNameLabel.Size = new System.Drawing.Size(174, 23); + this.AppNameLabel.TabIndex = 1; + this.AppNameLabel.Text = "ValheimServerGUI"; + this.AppNameLabel.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; + // + // ProgressBar + // + this.ProgressBar.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.ProgressBar.Location = new System.Drawing.Point(12, 35); + this.ProgressBar.Name = "ProgressBar"; + this.ProgressBar.Size = new System.Drawing.Size(174, 16); + this.ProgressBar.Style = System.Windows.Forms.ProgressBarStyle.Continuous; + this.ProgressBar.TabIndex = 2; + // + // SplashForm + // + this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(198, 63); + this.ControlBox = false; + this.Controls.Add(this.AppNameLabel); + this.Controls.Add(this.ProgressBar); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle; + this.Name = "SplashForm"; + this.ShowInTaskbar = false; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; + this.ResumeLayout(false); + + } + + #endregion + private System.Windows.Forms.Label AppNameLabel; + private System.Windows.Forms.ProgressBar ProgressBar; + } +} \ No newline at end of file diff --git a/ValheimServerGUI/Forms/SplashForm.cs b/ValheimServerGUI/Forms/SplashForm.cs new file mode 100644 index 0000000..03b5f95 --- /dev/null +++ b/ValheimServerGUI/Forms/SplashForm.cs @@ -0,0 +1,292 @@ +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Windows.Forms; +using ValheimServerGUI.Properties; +using ValheimServerGUI.Tools; + +namespace ValheimServerGUI.Forms +{ + public partial class SplashForm : Form + { +#if DEBUG + private static readonly bool SimulateLongRunningStartup = false; + private static readonly bool SimulateStartupTaskException = false; + private static readonly bool SimulateAsyncPopoutOnStart = false; +#endif + private Form MainForm; + private bool IsFirstShown = true; + private bool CloseAfterExceptionHandled = false; + + private readonly List> StartupTasks = new(); + private readonly List FinishedTasks = new(); + private event EventHandler TaskFinished; + + private readonly IFormProvider FormProvider; + private readonly IIpAddressProvider IpAddressProvider; + private readonly ISoftwareUpdateProvider SoftwareUpdateProvider; + private readonly IExceptionHandler ExceptionHandler; + private readonly ILogger Logger; + + public SplashForm( + IFormProvider formProvider, + IIpAddressProvider ipAddressProvider, + ISoftwareUpdateProvider softwareUpdateProvider, + IExceptionHandler exceptionHandler, + ILogger logger) + { + this.FormProvider = formProvider; + this.IpAddressProvider = ipAddressProvider; + this.SoftwareUpdateProvider = softwareUpdateProvider; + this.ExceptionHandler = exceptionHandler; + this.Logger = logger; + + Application.ThreadException += Application_ThreadException; + AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; + + try + { + InitializeComponent(); + this.AddApplicationIcon(); + InitializeAppName(); + InitializeFormEvents(); + } + catch (Exception e) + { + this.HandleException(e, "Startup Init Exception", true); + } + } + + private void InitializeAppName() + { + this.AppNameLabel.Text = $"ValheimServerGUI v{AssemblyHelper.GetApplicationVersion()}"; + } + + private void InitializeFormEvents() + { + this.Shown += this.BuildEventHandler(this.SplashForm_OnShown); + this.ExceptionHandler.ExceptionHandled += this.BuildEventHandler(this.OnExceptionHandled); + } + + #region Form events + + protected void SplashForm_OnShown() + { + if (this.IsFirstShown) + { + this.IsFirstShown = false; + + // For some reason the form is not actually fully rendered at this point + // (labels appear as white boxes) so I'm forcing a redraw here + this.Refresh(); + + this.OnFirstShown(); + } + } + + protected void OnFirstShown() + { + try + { + if (!this.VersionCheck()) + { + this.Close(); + return; + } + + InitializeMainForm(); + InitializeStartupTasks(); + RunStartupTasks(); + } + catch (Exception ex) + { + this.HandleException(ex, "Startup Run Exception", true); + } + } + + private void InitializeMainForm() + { + this.MainForm = this.FormProvider.GetForm(); + + // Since the splash screen is the application's main form, it must continue running in the background + // So listen for whenever the MainWindow closes, and close the splash screen as well, in order to close the application + this.MainForm.FormClosed += this.OnMainFormClosed; + } + + private void InitializeStartupTasks() + { + this.TaskFinished += this.BuildEventHandler(this.OnTaskFinished); + + this.AddStartupTask(this.IpAddressProvider.GetExternalIpAddressAsync); + this.AddStartupTask(this.IpAddressProvider.GetInternalIpAddressAsync); + this.AddStartupTask(() => this.SoftwareUpdateProvider.CheckForUpdatesAsync(false)); + +#if DEBUG + if (SimulateLongRunningStartup) + { + this.AddStartupTask(() => Task.Delay(2000)); + this.AddStartupTask(() => Task.Delay(2500)); + this.AddStartupTask(() => Task.Delay(3000)); + } + + if (SimulateStartupTaskException) + { + this.AddStartupTask(async () => + { + await Task.Delay(500); + throw new InvalidOperationException("Intentional exception thrown for testing"); + }); + } +#endif + } + + #endregion + + #region Event handlers + + private void OnTaskFinished(Task task) + { + if (!this.StartupTasks.Any()) + { + // Close the splash screen if there are no startup tasks + this.FinishStartup(); + return; + } + + if (task != null) + { + this.FinishedTasks.Add(task); + + if (!task.IsCompletedSuccessfully) + { + this.Logger.LogWarning("Error encountered during startup task"); + this.HandleException(task.Exception, "Startup Task Exception", true); + return; + } + + //this.Logger.LogTrace($"Finishing startup task #{this.FinishedTasks.Count}"); + } + + var numTasks = this.StartupTasks.Count; + var numTasksFinished = this.FinishedTasks.Count; + var pctTasksFinished = numTasksFinished * 100 / numTasks; + + this.ProgressBar.Value = pctTasksFinished; + + if (numTasksFinished >= numTasks) + { + // Close the splash screen once all startup tasks have finished + this.FinishStartup(); + return; + } + } + + private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) + { + var isMainFormVisible = this.MainForm != null && this.MainForm.Visible; + this.HandleException(e.ExceptionObject as Exception, "Unhandled Exception", !isMainFormVisible); + } + + private void Application_ThreadException(object sender, System.Threading.ThreadExceptionEventArgs e) + { + var isMainFormVisible = this.MainForm != null && this.MainForm.Visible; + this.HandleException(e.Exception, "Thread Exception", !isMainFormVisible); + } + + private void OnExceptionHandled() + { + if (CloseAfterExceptionHandled) this.Close(); + } + + private void OnMainFormClosed(object sender, FormClosedEventArgs e) + { + this.Close(); + } + + #endregion + + #region Common methods + + private void AddStartupTask(Func taskFunc) + { + this.StartupTasks.Add(taskFunc); + } + + private void RunStartupTasks() + { + //var i = 1; + foreach (var taskFunc in this.StartupTasks) + { + //this.Logger.LogTrace($"Beginning startup task #{i++}"); + + Task.Run(() => taskFunc().ContinueWith(t => + { + this.TaskFinished?.Invoke(this, t); + return Task.CompletedTask; + })); + } + } + + private bool VersionCheck() + { + var dotnetVersion = AssemblyHelper.GetDotnetRuntimeVersion(); + + if (dotnetVersion.Major < 5) + { + this.Logger.LogWarning($"Incompatible .NET version detected: {dotnetVersion}"); + + var nl = Environment.NewLine; + var result = MessageBox.Show( + $"ValheimServerGUI requires the .NET 5.0 Desktop Runtime (or higher) to be installed.{nl}" + + $"You are currently using .NET {dotnetVersion}.{nl}{nl}" + + "Would you like to go to the download page now?", + ".NET Upgrade Required", + MessageBoxButtons.YesNo, + MessageBoxIcon.Warning); + + if (result == DialogResult.Yes) + { + WebHelper.OpenWebAddress(Resources.UrlDotnetDownload); + } + + return false; + } + + return true; + } + + private void HandleException(Exception exception, string contextMessage, bool closeAfterHandle) + { + this.Logger.LogError($"Encountered exception - {exception.GetType().Name}: {exception.Message}"); + + this.CloseAfterExceptionHandled = closeAfterHandle; + + this.ExceptionHandler.HandleException(exception, contextMessage); + } + + private void FinishStartup() + { + this.MainForm.Show(); +#if DEBUG + if (SimulateAsyncPopoutOnStart) + { + var asyncPopout = new AsyncPopout(Task.Delay(5000), options => + { + options.Text = "Testing AsyncPopout..."; + options.Title = "Testing AsyncPopout"; + options.SuccessMessage = "Task succeeded!"; + options.FailureMessage = "Task failed!"; + }); + + asyncPopout.Show(); + } +#endif + // Hide the splash screen so it's no longer visible once the application is loaded + this.Hide(); + } + + #endregion + } +} diff --git a/ValheimServerGUI/Forms/SplashForm.resx b/ValheimServerGUI/Forms/SplashForm.resx new file mode 100644 index 0000000..f298a7b --- /dev/null +++ b/ValheimServerGUI/Forms/SplashForm.resx @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/ValheimServerGUI/Program.cs b/ValheimServerGUI/Program.cs index 76df621..852440c 100644 --- a/ValheimServerGUI/Program.cs +++ b/ValheimServerGUI/Program.cs @@ -4,7 +4,6 @@ using System.Windows.Forms; using ValheimServerGUI.Forms; using ValheimServerGUI.Game; -using ValheimServerGUI.Properties; using ValheimServerGUI.Tools; using ValheimServerGUI.Tools.Data; using ValheimServerGUI.Tools.Http; @@ -23,13 +22,9 @@ public static class Program [STAThread] public static void Main() { - if (!VersionCheck()) return; - Application.SetHighDpiMode(HighDpiMode.SystemAware); Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); - Application.ThreadException += Application_ThreadException; - AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; var services = new ServiceCollection(); ConfigureServices(services); @@ -38,7 +33,7 @@ public static void Main() try { - Application.Run(serviceProvider.GetRequiredService()); + Application.Run(serviceProvider.GetRequiredService()); } catch (Exception e) { @@ -62,7 +57,9 @@ public static void ConfigureServices(IServiceCollection services) .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton() + .AddSingleton(); // Game & server data services @@ -74,47 +71,13 @@ public static void ConfigureServices(IServiceCollection services) // Forms services + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton() .AddTransient(); } - - private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) - { - ExceptionHandler.HandleException(e.ExceptionObject as Exception, "Unhandled Exception"); - } - - private static void Application_ThreadException(object sender, System.Threading.ThreadExceptionEventArgs e) - { - ExceptionHandler.HandleException(e.Exception, "Thread Exception"); - } - - private static bool VersionCheck() - { - var dotnetVersion = AssemblyHelper.GetDotnetRuntimeVersion(); - - if (dotnetVersion.Major < 5) - { - var nl = Environment.NewLine; - var result = MessageBox.Show( - $"ValheimServerGUI requires the .NET 5.0 Desktop Runtime (or higher) to be installed.{nl}" + - $"You are currently using .NET {dotnetVersion}.{nl}{nl}" + - "Would you like to go to the download page now?", - ".NET Upgrade Required", - MessageBoxButtons.YesNo, - MessageBoxIcon.Warning); - - if (result == DialogResult.Yes) - { - WebHelper.OpenWebAddress(Resources.UrlDotnetDownload); - } - - return false; - } - - return true; - } } } diff --git a/ValheimServerGUI/Properties/Resources.Designer.cs b/ValheimServerGUI/Properties/Resources.Designer.cs index 9201b01..74d12d2 100644 --- a/ValheimServerGUI/Properties/Resources.Designer.cs +++ b/ValheimServerGUI/Properties/Resources.Designer.cs @@ -176,6 +176,16 @@ internal static System.Drawing.Bitmap Loading_Blue_16x { } } + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap NewBug_16x { + get { + object obj = ResourceManager.GetObject("NewBug_16x", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + /// /// Looks up a localized resource of type System.Drawing.Bitmap. /// @@ -426,6 +436,15 @@ internal static string UrlPortForwardingGuide { } } + /// + /// Looks up a localized string similar to https://api.runeberry.com/vsg-api. + /// + internal static string UrlRuneberryApi { + get { + return ResourceManager.GetString("UrlRuneberryApi", resourceCulture); + } + } + /// /// Looks up a localized string similar to https://twitter.com/Runeberries. /// diff --git a/ValheimServerGUI/Properties/Resources.resx b/ValheimServerGUI/Properties/Resources.resx index a6a5b92..5b5bb0c 100644 --- a/ValheimServerGUI/Properties/Resources.resx +++ b/ValheimServerGUI/Properties/Resources.resx @@ -154,6 +154,9 @@ ..\Resources\Loading_Blue_16x.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + ..\Resources\NewBug_16x.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + ..\Resources\OpenWeb_16x.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a @@ -232,6 +235,9 @@ https://github.com/runeberry/ValheimServerGUI/wiki/Connecting-to-your-Server + + https://api.runeberry.com/vsg-api + https://twitter.com/Runeberries diff --git a/ValheimServerGUI/Resources/NewBug_16x.png b/ValheimServerGUI/Resources/NewBug_16x.png new file mode 100644 index 0000000..f6ce855 Binary files /dev/null and b/ValheimServerGUI/Resources/NewBug_16x.png differ diff --git a/ValheimServerGUI/Tools/AssemblyHelper.cs b/ValheimServerGUI/Tools/AssemblyHelper.cs index a703fab..821c06f 100644 --- a/ValheimServerGUI/Tools/AssemblyHelper.cs +++ b/ValheimServerGUI/Tools/AssemblyHelper.cs @@ -1,6 +1,10 @@ -using System; +using DeviceId; +using System; +using System.Globalization; using System.Linq; using System.Reflection; +using System.Security.Cryptography; +using System.Text; namespace ValheimServerGUI.Tools { @@ -34,5 +38,39 @@ public static Version GetDotnetRuntimeVersion() { return Environment.Version; } + + private static string ClientCorrelationId; + + public static string GetClientCorrelationId() + { + if (ClientCorrelationId != null) return ClientCorrelationId; + + var deviceId = new DeviceIdBuilder() + .AddMacAddress() + .AddMotherboardSerialNumber() + .ToString() + .ToLowerInvariant(); + + using var hash = MD5.Create(); + var hexStrings = hash.ComputeHash(Encoding.UTF8.GetBytes(deviceId)).Select(b => b.ToString("x2")); + ClientCorrelationId = string.Join(string.Empty, hexStrings); + + return ClientCorrelationId; + } + + public static CrashReport BuildCrashReport() + { + return new CrashReport + { + CrashReportId = Guid.NewGuid().ToString(), + ClientCorrelationId = GetClientCorrelationId(), + Timestamp = DateTime.UtcNow, + AppVersion = GetApplicationVersion(), + OsVersion = Environment.OSVersion.VersionString, + DotnetVersion = Environment.Version.ToString(), + CurrentCulture = CultureInfo.CurrentCulture?.ToString(), + CurrentUICulture = CultureInfo.CurrentUICulture?.ToString(), + }; + } } } diff --git a/ValheimServerGUI/Tools/ExceptionExtensions.cs b/ValheimServerGUI/Tools/ExceptionExtensions.cs new file mode 100644 index 0000000..5c9604c --- /dev/null +++ b/ValheimServerGUI/Tools/ExceptionExtensions.cs @@ -0,0 +1,17 @@ +using System; + +namespace ValheimServerGUI.Tools +{ + public static class ExceptionExtensions + { + public static Exception GetPrimaryException(this Exception exception) + { + if (exception is AggregateException agg) + { + return agg.InnerException.GetPrimaryException() ?? agg; + } + + return exception; + } + } +} diff --git a/ValheimServerGUI/Tools/ExceptionHandler.cs b/ValheimServerGUI/Tools/ExceptionHandler.cs index b193a12..225ebef 100644 --- a/ValheimServerGUI/Tools/ExceptionHandler.cs +++ b/ValheimServerGUI/Tools/ExceptionHandler.cs @@ -1,87 +1,86 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Net.Mail; -using System.Text; -using System.Threading.Tasks; using System.Windows.Forms; +using ValheimServerGUI.Forms; +using ValheimServerGUI.Tools.Logging; namespace ValheimServerGUI.Tools { public interface IExceptionHandler { - void HandleException(Exception e, string additionalMessage = null); + event EventHandler ExceptionHandled; + + void HandleException(Exception e, string contextMessage = null); } public class ExceptionHandler : IExceptionHandler { - private static readonly string NL = Environment.NewLine; + private readonly IRuneberryApiClient RuneberryApiClient; + + private readonly IEventLogger Logger; - public void HandleException(Exception e, string additionalMessage = null) + public ExceptionHandler(IRuneberryApiClient runeberryApiClient, IEventLogger logger) + { + RuneberryApiClient = runeberryApiClient; + Logger = logger; + } + + public event EventHandler ExceptionHandled; + + public void HandleException(Exception e, string contextMessage = null) { if (e == null) return; - additionalMessage ??= "Unhandled Exception"; - var message = "An unhandled exception has been thrown, and ValheimServerGUI will be terminated."; - //var stackTrace = string.Join(NL, e.StackTrace.Split(NL).Take(3)); - message += $"{NL}{NL}{BuildMessageBody(e, additionalMessage)}"; + e = e.GetPrimaryException(); + + contextMessage ??= "Unknown Exception"; + var userMessage = "A fatal error has occured. Would you like to send an automated crash report to the developer?"; var result = MessageBox.Show( - message, - additionalMessage, - MessageBoxButtons.OK, + userMessage, + contextMessage, + MessageBoxButtons.YesNo, MessageBoxIcon.Error); - //if (result == DialogResult.Yes) - //{ - // try - // { - // var body = BuildMessageBody(e, additionalMessage); - // SendEmail("ValheimServerGUI - Automated bug report", additionalMessage); - - // MessageBox.Show("Bug report sent. Thank you!", additionalMessage, MessageBoxButtons.OK, MessageBoxIcon.Information); - // } - // catch(Exception e2) - // { - // MessageBox.Show("Failed to send bug report. Sorry!" + e2.Message, additionalMessage, MessageBoxButtons.OK, MessageBoxIcon.Warning); - // } - //} - } - - //private void SendEmail(string subject, string body) - //{ - // var mailClient = new SmtpClient(""); - // var mailMessage = new MailMessage(); + if (result == DialogResult.Yes) + { + var crashReport = BuildCrashReport(e, contextMessage); + var task = RuneberryApiClient.SendCrashReportAsync(crashReport); - // mailMessage.From = new MailAddress(""); - // mailMessage.From = new MailAddress(""); - // mailMessage.To.Add(new MailAddress("")); + var asyncPopout = new AsyncPopout(task, o => + { + o.Title = "Crash Report"; + o.Text = "Sending crash report..."; + o.SuccessMessage = "Crash report received. Thank you!"; + o.FailureMessage = "Failed to send crash report.\r\nContact Runeberry Software for further support."; + }); - // mailMessage.Subject = subject; - // mailMessage.Body = body; + asyncPopout.ShowDialog(); + } - // mailClient.Send(mailMessage); - // mailMessage.Dispose(); - //} + this.ExceptionHandled?.Invoke(this, EventArgs.Empty); + } - private string BuildMessageBody(Exception e, string additionalMessage) + private CrashReport BuildCrashReport(Exception e, string contextMessage) { - var os = Environment.OSVersion; - - var body = - $"{e.GetType().Name}: {e.Message}{NL}" + - $"Timestamp: {DateTime.UtcNow:O}{NL}" + - $"Context: {additionalMessage}{NL}" + - $"Source: {e.Source}{NL}" + - $"TargetSite: {e.TargetSite}{NL}" + - NL + - $"ValheimServerGUI version: {AssemblyHelper.GetApplicationVersion()}{NL}" + - $"OS Version: {os.VersionString}{NL}" + - $".NET Version: {Environment.Version}" + - NL + - $"Stack trace:{NL}{e.StackTrace}"; - - return body; + var crashReport = AssemblyHelper.BuildCrashReport(); + + var additionalInfo = new Dictionary + { + { "ExceptionType", e.GetType().Name }, + { "Message", e.Message }, + { "Context", contextMessage }, + { "Source", e.Source }, + { "TargetSite", e.TargetSite?.ToString() }, + { "StackTrace", e.StackTrace }, + }; + + crashReport.Source = "CrashReport"; + crashReport.AdditionalInfo = additionalInfo; + crashReport.Logs = this.Logger.LogBuffer.Reverse().Take(100).ToList(); + + return crashReport; } } } diff --git a/ValheimServerGUI/Tools/GitHubClient.cs b/ValheimServerGUI/Tools/GitHubClient.cs index 64d1bb9..6721ca3 100644 --- a/ValheimServerGUI/Tools/GitHubClient.cs +++ b/ValheimServerGUI/Tools/GitHubClient.cs @@ -24,6 +24,11 @@ public async Task GetLatestReleaseAsync() .WithHeader("User-Agent", "ValheimServerGUI") .SendAsync(); + if (releases == null) + { + throw new Exception("Unable to reach GitHub."); + } + var latestRelease = releases .Where(r => r.Assets != null && r.Assets.Any()) .Where(r => !r.Prerelease && !r.Draft) diff --git a/ValheimServerGUI/Tools/IpAddressProvider.cs b/ValheimServerGUI/Tools/IpAddressProvider.cs index f8d0727..0eaf601 100644 --- a/ValheimServerGUI/Tools/IpAddressProvider.cs +++ b/ValheimServerGUI/Tools/IpAddressProvider.cs @@ -73,7 +73,6 @@ public Task GetInternalIpAddressAsync() if (result != null) { - this.Logger.LogTrace("Found {0} internal IP address(es): {1}", results.Count(), string.Join(", ", results)); this.InternalIpReceived?.Invoke(this, result); } diff --git a/ValheimServerGUI/Tools/RuneberryApiClient.cs b/ValheimServerGUI/Tools/RuneberryApiClient.cs new file mode 100644 index 0000000..4b71914 --- /dev/null +++ b/ValheimServerGUI/Tools/RuneberryApiClient.cs @@ -0,0 +1,58 @@ +using Newtonsoft.Json; +using System; +using System.Threading.Tasks; +using ValheimServerGUI.Properties; +using ValheimServerGUI.Tools.Http; + +namespace ValheimServerGUI.Tools +{ + public interface IRuneberryApiClient + { + Task SendCrashReportAsync(CrashReport report); + } + + public class RuneberryApiClient : RestClient, IRuneberryApiClient + { + public RuneberryApiClient(IRestClientContext context) : base(context) + { + } + + public async Task SendCrashReportAsync(CrashReport report) + { + var response = await this.Post($"{Resources.UrlRuneberryApi}/crash-report", report) + .WithHeader(Secrets.RuneberryApiKeyHeader, Secrets.RuneberryClientApiKey) + .SendAsync(); + + if (response == null || !response.IsSuccessStatusCode) + { + string message; + + try + { + if (response != null) + { + var rawResponse = await response.Content.ReadAsStringAsync(); + var exceptionResponse = JsonConvert.DeserializeObject(rawResponse); + message = $"({(int)response.StatusCode}) {exceptionResponse.Message}"; + } + else + { + message = "Unable to reach Runeberry API"; + } + } + catch + { + message = "Unknown error"; + } + + throw new Exception(message); + } + } + + private class ExceptionResponse + { + [JsonProperty("message")] + public string Message { get; set; } + } + } +} diff --git a/ValheimServerGUI/Tools/SoftwareUpdateProvider.cs b/ValheimServerGUI/Tools/SoftwareUpdateProvider.cs new file mode 100644 index 0000000..05c93b9 --- /dev/null +++ b/ValheimServerGUI/Tools/SoftwareUpdateProvider.cs @@ -0,0 +1,106 @@ +using System; +using System.Threading.Tasks; +using ValheimServerGUI.Game; +using ValheimServerGUI.Properties; + +namespace ValheimServerGUI.Tools +{ + public interface ISoftwareUpdateProvider + { + event EventHandler UpdateCheckStarted; + + event EventHandler UpdateCheckFinished; + + Task CheckForUpdatesAsync(bool isManualCheck); + } + + public class SoftwareUpdateEventArgs + { + public string LatestVersion { get; } + + public bool IsNewerVersionAvailable { get; } + + public bool IsManualCheck { get; } + + public bool IsSuccessful { get; } + + public Exception Exception { get; } + + public SoftwareUpdateEventArgs( + string latestVersion, + bool isNewerVersionAvailable, + bool isManualCheck) + { + this.LatestVersion = latestVersion; + this.IsNewerVersionAvailable = isNewerVersionAvailable; + this.IsManualCheck = isManualCheck; + this.IsSuccessful = true; + } + + public SoftwareUpdateEventArgs( + Exception e, + bool isManualCheck) + { + this.Exception = e; + this.IsNewerVersionAvailable = false; + this.IsManualCheck = isManualCheck; + this.IsSuccessful = false; + } + } + + public class SoftwareUpdateProvider : ISoftwareUpdateProvider + { + private readonly IGitHubClient GitHubClient; + private readonly IUserPreferencesProvider UserPrefsProvider; + + private readonly TimeSpan UpdateCheckInterval = TimeSpan.Parse(Resources.UpdateCheckInterval); + private DateTime NextAutomaticUpdateCheck = DateTime.MinValue; + + public SoftwareUpdateProvider(IGitHubClient gitHubClient, IUserPreferencesProvider userPrefsProvider) + { + this.GitHubClient = gitHubClient; + this.UserPrefsProvider = userPrefsProvider; + } + + public event EventHandler UpdateCheckStarted; + + public event EventHandler UpdateCheckFinished; + + public async Task CheckForUpdatesAsync(bool isManualCheck) + { + if (!isManualCheck) + { + // Only fulfill automated checks if enough time has passed since the last check + var now = DateTime.UtcNow; + if (now < this.NextAutomaticUpdateCheck) return; + this.NextAutomaticUpdateCheck = now + this.UpdateCheckInterval; + + // Only fulfill automated checks if the user has update checks enabled + var prefs = this.UserPrefsProvider.LoadPreferences(); + if (!prefs.CheckForUpdates) return; + } + + this.UpdateCheckStarted?.Invoke(this, EventArgs.Empty); + + SoftwareUpdateEventArgs eventArgs; + + try + { + var currentVersion = AssemblyHelper.GetApplicationVersion(); + var release = await this.GitHubClient.GetLatestReleaseAsync(); + var newerVersionAvailable = AssemblyHelper.IsNewerVersion(release?.TagName); + + // In case there was no response from GitHub, consider the current running version as the "latest version" + var latestVersion = release?.TagName ?? AssemblyHelper.GetApplicationVersion(); + + eventArgs = new SoftwareUpdateEventArgs(latestVersion, newerVersionAvailable, isManualCheck); + } + catch (Exception e) + { + eventArgs = new SoftwareUpdateEventArgs(e, isManualCheck); + } + + this.UpdateCheckFinished?.Invoke(this, eventArgs); + } + } +} diff --git a/ValheimServerGUI/Tools/WinFormsExtensions.cs b/ValheimServerGUI/Tools/WinFormsExtensions.cs index df522fe..27f943e 100644 --- a/ValheimServerGUI/Tools/WinFormsExtensions.cs +++ b/ValheimServerGUI/Tools/WinFormsExtensions.cs @@ -6,6 +6,7 @@ using System.Resources; using System.Threading.Tasks; using System.Windows.Forms; +using ValheimServerGUI.Properties; namespace ValheimServerGUI.Tools { @@ -81,6 +82,17 @@ await Task.Run(() => }; } + /// + /// (jb, 5/9/21) For some reason, you cannot set a Form's icon from a Resource in the Designer, so I've been setting it + /// using the file browser. However, I think this might be causing an issue when publishing the application as a trimmed + /// single-file executable - some users are encountering errors when trying to load *some image* on startup, and I think + /// this might be it. + /// + public static void AddApplicationIcon(this Form form) + { + form.Icon = Resources.ApplicationIcon; + } + #endregion #region TextBox extensions @@ -104,7 +116,7 @@ public static void AppendLine(this TextBox textBox, string line) public static void AddImagesFromResourceFile(this ImageList list, Type resourcesType) { var resourceImages = new ResourceManager(resourcesType) - .GetResourceSet(CultureInfo.CurrentUICulture, true, true) + .GetResourceSet(CultureInfo.InvariantCulture, true, true) .Cast() .Where(de => de.Key != null && de.Value != null && typeof(Image).IsAssignableFrom(de.Value.GetType())) .ToDictionary(de => de.Key.ToString(), de => de.Value as Image); diff --git a/ValheimServerGUI/ValheimServerGUI.csproj b/ValheimServerGUI/ValheimServerGUI.csproj index 1006267..d9fb835 100644 --- a/ValheimServerGUI/ValheimServerGUI.csproj +++ b/ValheimServerGUI/ValheimServerGUI.csproj @@ -5,27 +5,36 @@ net5.0-windows true Resources\ApplicationIcon.ico - true - ValheimServerGUI.snk Runeberry Software, LLC ValheimServerGUI A simple user interface for running Valheim Dedicated Server on Windows. 2021 GNU GPLv3 - 1.2.5 + 1.3.0 + + + true + ..\SolutionResources\ValheimServerGUI.snk + - + + + + + + + True @@ -41,10 +50,4 @@ - - - PreserveNewest - - - \ No newline at end of file