250 lines
9.1 KiB
C#
250 lines
9.1 KiB
C#
|
|
using System;
|
|||
|
|
using System.Diagnostics;
|
|||
|
|
using System.IO;
|
|||
|
|
using System.Text.Json;
|
|||
|
|
using System.Threading;
|
|||
|
|
using System.Threading.Tasks;
|
|||
|
|
using Microsoft.AspNetCore.Builder;
|
|||
|
|
using Microsoft.AspNetCore.Http;
|
|||
|
|
using Microsoft.Data.SqlClient;
|
|||
|
|
using Microsoft.Extensions.Configuration;
|
|||
|
|
using Microsoft.Extensions.DependencyInjection;
|
|||
|
|
using Microsoft.Extensions.Hosting;
|
|||
|
|
using Serilog;
|
|||
|
|
using static Sozsoft.Settings.SettingsConsts;
|
|||
|
|
|
|||
|
|
namespace Sozsoft.Platform;
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// Veritabanı henüz hazır değilken çalışan minimal kurulum uygulaması.
|
|||
|
|
/// Tam ABP stack yüklemez; sadece /api/setup/* endpointlerini sunar.
|
|||
|
|
/// </summary>
|
|||
|
|
internal static class SetupAppRunner
|
|||
|
|
{
|
|||
|
|
// Veritabanı Hazırlık Kontrolü
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// DB var mı ve AbpRoles tablosu oluşmuş mu diye kontrol eder.
|
|||
|
|
/// Boş DB veya bağlantı hatası durumunda false döner.
|
|||
|
|
/// </summary>
|
|||
|
|
public static bool DatabaseIsReady(IConfiguration configuration)
|
|||
|
|
{
|
|||
|
|
var connectionString = configuration.GetConnectionString(DefaultDatabaseProvider);
|
|||
|
|
if (string.IsNullOrWhiteSpace(connectionString))
|
|||
|
|
return false;
|
|||
|
|
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
if (DefaultDatabaseProvider == DatabaseProvider.SqlServer)
|
|||
|
|
return SqlServerIsReady(connectionString);
|
|||
|
|
|
|||
|
|
#pragma warning disable CS0162
|
|||
|
|
return true; // Diğer sağlayıcılar için geçici — ileride PostgreSQL desteği eklenecek
|
|||
|
|
#pragma warning restore CS0162
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
Log.Warning("Veritabanı hazırlık kontrolü başarısız: {Error}", ex.Message);
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static bool SqlServerIsReady(string connectionString)
|
|||
|
|
{
|
|||
|
|
var csb = new SqlConnectionStringBuilder(connectionString);
|
|||
|
|
var dbName = csb.InitialCatalog;
|
|||
|
|
if (string.IsNullOrEmpty(dbName))
|
|||
|
|
return false;
|
|||
|
|
|
|||
|
|
// 1) master'a bağlan — DB varlığını kontrol et
|
|||
|
|
var masterCsb = new SqlConnectionStringBuilder(connectionString)
|
|||
|
|
{
|
|||
|
|
InitialCatalog = "master",
|
|||
|
|
ConnectTimeout = 8
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
using var masterConn = new SqlConnection(masterCsb.ConnectionString);
|
|||
|
|
masterConn.Open();
|
|||
|
|
|
|||
|
|
using var dbCheck = new SqlCommand(
|
|||
|
|
"SELECT COUNT(1) FROM sys.databases WHERE name = @n", masterConn);
|
|||
|
|
dbCheck.Parameters.AddWithValue("@n", dbName);
|
|||
|
|
if ((int)dbCheck.ExecuteScalar() == 0)
|
|||
|
|
return false;
|
|||
|
|
|
|||
|
|
// 2) Hedef DB'ye bağlan — AbpRoles tablosunun varlığını kontrol et
|
|||
|
|
csb.ConnectTimeout = 8;
|
|||
|
|
using var dbConn = new SqlConnection(csb.ConnectionString);
|
|||
|
|
dbConn.Open();
|
|||
|
|
|
|||
|
|
using var tableCheck = new SqlCommand(
|
|||
|
|
"SELECT COUNT(1) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'AbpRoles'",
|
|||
|
|
dbConn);
|
|||
|
|
return (int)tableCheck.ExecuteScalar() > 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Minimal Kurulum Uygulaması
|
|||
|
|
|
|||
|
|
public static async Task<int> RunAsync(string[] args, IConfiguration configuration)
|
|||
|
|
{
|
|||
|
|
Log.Warning("Veritabanı hazır değil — kurulum modu başlatılıyor.");
|
|||
|
|
|
|||
|
|
var builder = WebApplication.CreateBuilder(args);
|
|||
|
|
|
|||
|
|
var extraOrigins = (configuration["App:CorsOrigins"] ?? "")
|
|||
|
|
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
|||
|
|
var baseDomain = configuration["App:BaseDomain"]?.Trim();
|
|||
|
|
|
|||
|
|
builder.Services.AddCors(o => o.AddPolicy("Setup", policy =>
|
|||
|
|
policy.AllowAnyHeader()
|
|||
|
|
.AllowAnyMethod()
|
|||
|
|
.AllowCredentials()
|
|||
|
|
.SetIsOriginAllowed(origin =>
|
|||
|
|
{
|
|||
|
|
if (!Uri.TryCreate(origin, UriKind.Absolute, out var uri)) return false;
|
|||
|
|
var host = uri.Host.ToLowerInvariant();
|
|||
|
|
|
|||
|
|
if (host is "localhost" or "127.0.0.1" or "[::1]")
|
|||
|
|
return true;
|
|||
|
|
|
|||
|
|
if (!string.IsNullOrWhiteSpace(baseDomain))
|
|||
|
|
{
|
|||
|
|
var bd = baseDomain.ToLowerInvariant();
|
|||
|
|
if (host == bd || host.EndsWith("." + bd))
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
foreach (var o in extraOrigins)
|
|||
|
|
if (Uri.TryCreate(o, UriKind.Absolute, out var eo) &&
|
|||
|
|
eo.Host.Equals(host, StringComparison.OrdinalIgnoreCase))
|
|||
|
|
return true;
|
|||
|
|
|
|||
|
|
return false;
|
|||
|
|
})));
|
|||
|
|
|
|||
|
|
builder.Host.UseSerilog();
|
|||
|
|
|
|||
|
|
var app = builder.Build();
|
|||
|
|
app.UseCors("Setup");
|
|||
|
|
|
|||
|
|
app.MapGet("/api/setup/status", (IConfiguration cfg) =>
|
|||
|
|
Results.Ok(new { dbExists = DatabaseIsReady(cfg) }));
|
|||
|
|
|
|||
|
|
app.MapGet("/api/setup/migrate", async (IConfiguration cfg, IHostEnvironment env,
|
|||
|
|
IHostApplicationLifetime lifetime, HttpContext ctx, CancellationToken ct) =>
|
|||
|
|
{
|
|||
|
|
ctx.Response.ContentType = "text/event-stream; charset=utf-8";
|
|||
|
|
ctx.Response.Headers["Cache-Control"] = "no-cache, no-store";
|
|||
|
|
ctx.Response.Headers["X-Accel-Buffering"] = "no";
|
|||
|
|
await ctx.Response.Body.FlushAsync(ct);
|
|||
|
|
|
|||
|
|
async Task Send(string level, string message)
|
|||
|
|
{
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
var payload = JsonSerializer.Serialize(new { level, message });
|
|||
|
|
await ctx.Response.WriteAsync($"data: {payload}\n\n", ct);
|
|||
|
|
await ctx.Response.Body.FlushAsync(ct);
|
|||
|
|
}
|
|||
|
|
catch { }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (DatabaseIsReady(cfg))
|
|||
|
|
{
|
|||
|
|
await Send("warn", "Veritabanı zaten hazır. Migration atlanıyor.");
|
|||
|
|
await Send("done", "Tamamlandı.");
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var migratorPath = cfg["Setup:MigratorPath"]
|
|||
|
|
?? Path.GetFullPath(Path.Combine(env.ContentRootPath, "..", "Sozsoft.Platform.DbMigrator"));
|
|||
|
|
|
|||
|
|
await Send("info", "Veritabanı migration ve seed başlatılıyor...");
|
|||
|
|
await Send("info", $"Migrator yolu: {migratorPath}");
|
|||
|
|
|
|||
|
|
var fileName = "dotnet";
|
|||
|
|
var arguments = migratorPath.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)
|
|||
|
|
? $"\"{migratorPath}\""
|
|||
|
|
: $"run --project \"{migratorPath}\"";
|
|||
|
|
|
|||
|
|
// seed=true her zaman geçirilir — başlangıç kurulumu için zorunlu.
|
|||
|
|
// appsettings Setup:MigratorArgs ile override edilebilir.
|
|||
|
|
var extraArgs = cfg["Setup:MigratorArgs"] ?? "seed=true";
|
|||
|
|
arguments += $" -- {extraArgs}";
|
|||
|
|
|
|||
|
|
await Send("info", $"Çalıştırılıyor: {fileName} {arguments}");
|
|||
|
|
|
|||
|
|
Process? process = null;
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
process = new Process
|
|||
|
|
{
|
|||
|
|
StartInfo = new ProcessStartInfo
|
|||
|
|
{
|
|||
|
|
FileName = fileName,
|
|||
|
|
Arguments = arguments,
|
|||
|
|
RedirectStandardOutput = true,
|
|||
|
|
RedirectStandardError = true,
|
|||
|
|
UseShellExecute = false,
|
|||
|
|
CreateNoWindow = true,
|
|||
|
|
WorkingDirectory = migratorPath,
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
process.Start();
|
|||
|
|
|
|||
|
|
async Task ReadStream(StreamReader reader, string level)
|
|||
|
|
{
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
while (!reader.EndOfStream)
|
|||
|
|
{
|
|||
|
|
var line = await reader.ReadLineAsync(ct);
|
|||
|
|
if (line != null) await Send(level, line);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
catch (OperationCanceledException) { }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
await Task.WhenAll(
|
|||
|
|
ReadStream(process.StandardOutput, "info"),
|
|||
|
|
ReadStream(process.StandardError, "warn"));
|
|||
|
|
|
|||
|
|
await process.WaitForExitAsync(ct);
|
|||
|
|
|
|||
|
|
if (process.ExitCode == 0)
|
|||
|
|
{
|
|||
|
|
await Send("success", "Migration ve seed başarıyla tamamlandı.");
|
|||
|
|
await Send("restart", "Uygulama sunucusu yeniden başlatılıyor...");
|
|||
|
|
await Send("done", "Tamamlandı.");
|
|||
|
|
|
|||
|
|
_ = Task.Delay(1500).ContinueWith(_ => lifetime.StopApplication());
|
|||
|
|
}
|
|||
|
|
else
|
|||
|
|
{
|
|||
|
|
await Send("error", $"Migration başarısız. Çıkış kodu: {process.ExitCode}");
|
|||
|
|
await Send("done", "Hata ile sonlandı.");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
catch (OperationCanceledException)
|
|||
|
|
{
|
|||
|
|
await Send("warn", "Migration isteği iptal edildi.");
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
await Send("error", $"Migration hatası: {ex.Message}");
|
|||
|
|
await Send("done", "Hata ile sonlandı.");
|
|||
|
|
}
|
|||
|
|
finally
|
|||
|
|
{
|
|||
|
|
process?.Dispose();
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
app.MapFallback(() => Results.StatusCode(503));
|
|||
|
|
|
|||
|
|
await app.RunAsync();
|
|||
|
|
return 0;
|
|||
|
|
}
|
|||
|
|
}
|