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;
///
/// Veritabanı henüz hazır değilken çalışan minimal kurulum uygulaması.
/// Tam ABP stack yüklemez; sadece /api/setup/* endpointlerini sunar.
///
internal static class SetupAppRunner
{
// Veritabanı Hazırlık Kontrolü
///
/// DB var mı ve AbpRoles tablosu oluşmuş mu diye kontrol eder.
/// Boş DB veya bağlantı hatası durumunda false döner.
///
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 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 extraArgs = cfg["Setup:MigratorArgs"] ?? "--Seed=true";
string fileName;
string arguments;
string workingDirectory;
if (migratorPath.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) && File.Exists(migratorPath))
{
// Doğrudan DLL yolu verilmiş — "--" separator YOK, doğrudan argüman
fileName = "dotnet";
arguments = $"\"{migratorPath}\" {extraArgs}";
workingDirectory = Path.GetDirectoryName(migratorPath)!;
}
else if (Directory.Exists(migratorPath))
{
// Klasör verilmiş — içinde publish edilmiş DLL var mı?
var dllFiles = Directory.GetFiles(migratorPath, "*.DbMigrator.dll", SearchOption.TopDirectoryOnly);
if (dllFiles.Length == 0)
dllFiles = Directory.GetFiles(migratorPath, "*Migrator*.dll", SearchOption.TopDirectoryOnly);
if (dllFiles.Length > 0)
{
// Publish çıktısı — SDK gerekmez, "--" separator YOK
fileName = "dotnet";
arguments = $"\"{dllFiles[0]}\" {extraArgs}";
workingDirectory = migratorPath;
}
else
{
// Kaynak proje klasörü — geliştirme ortamı, "--" gerekli
fileName = "dotnet";
arguments = $"run --project \"{migratorPath}\" -- {extraArgs}";
workingDirectory = migratorPath;
}
}
else
{
await Send("error", $"Migrator yolu bulunamadı veya geçersiz: {migratorPath}");
await Send("done", "Hata ile sonlandı.");
return;
}
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 = workingDirectory,
}
};
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;
}
}