Skip to content
Siber Vatan

Eklenti(Plugin) Sistemi ​

Muhammed Seyrek

Meriç Aytaç

Emrah Tusun

muhammedSeyrek/log-analyzer

Bir bilgisayar sistemi, transistörden uygulama eklentisine kadar tek bir bütün olarak tasarlanmaz. Her katman kendi altındaki katmanın ayrıntılarını gizler ve üstündeki katmana sınırlı, tanımlı bir arayüz sunar. İşletim sistemi işlemcinin elektriksel davranışını, uygulama disk denetleyicisinin komut setini, eklenti de uygulamanın iç veri yapılarını bilmek zorunda kalmaz.

Eklenti sistemi bu fikrin uygulama düzeyindeki karşılığıdır. Ana uygulama kararlı bir sözleşme tanımlar; eklentiler bu sözleşmeyi uygular; bir kayıt mekanizması da uygun eklentileri bulur, doğrular ve sisteme ekler. Bir aygıt sürücüsünün kernel'e kayıt olması ile bir eklentinin uygulamaya kayıt olması aynı tasarım problemidir: kararlı arayüz, sürüm uyumluluğu, yaşam döngüsü ve hata yalıtımı.

Bu yaklaşımın değeri yalnızca kodu ayrı dizinlere bölmekten gelmez. İyi tasarlanmış bir eklenti sistemi aşağıdaki soruları açıkça yanıtlar:

  • Eklenti hangi arayüzü uygulamalıdır?
  • Eklentiler nasıl keşfedilir ve sisteme bağlanır?
  • Uyumsuz veya hatalı bir eklenti nasıl reddedilir?
  • Eklentinin başlatılması ve kapatılması nasıl yönetilir?
  • Bir eklenti hatasının ana uygulamayı etkilemesi nasıl sınırlandırılır?
  • Sözleşme değiştiğinde geriye dönük uyumluluk nasıl korunur?

Yazının uygulama bölümü, Go ile yazılmış log-analyzer projesindeki eklenti sistemini örnek alır. Proje; log analizi, ağ telemetrisi ve akış işleme yeteneklerini çekirdeğe gömmek yerine birbirinden bağımsız üç eklenti olarak sunar.

Katmanlı Mimaride Eklentinin Yeri ​

Donanımdan uygulama eklentilerine uzanan yedi katman, eklenti sisteminin bütün içindeki yerini göstermek için kısa bir kavramsal model olarak kullanılabilir.

KatmanTemel SorumlulukSözleşme
1. Donanımİşlem, bellek, depolama ve giriş çıkış işlevlerini fiziksel olarak gerçekleştirir.Komut seti mimarisi
2. KernelSüreçleri, belleği, dosya sistemlerini ve aygıt erişimini yönetir.Sistem çağrıları
3. Donanım soyutlamaPlatforma özgü donanım ayrıntılarını ortak arayüzlerin arkasında toplar.Sürücü ve HAL arayüzleri
4. Kullanıcı alanı kütüphaneleriUygulamaların tekrar kullandığı ortak işlevleri sağlar.Kütüphane API'si ve sembolleri
5. API ve ABIBileşenlerin kaynak kodu veya ikili düzeyde nasıl haberleşeceğini tanımlar.Sürümlenmiş arayüz tanımı
6. Uygulamaİş kurallarını ve kullanıcıya sunulan temel davranışları yürütür.Kullanıcı ve servis arayüzleri
7. EklentiUygulamanın extension API'sine uyarak yeni davranış ekler.Extension API

Bilgi

Bu yedi katman standartlaştırılmış bir referans modeli değildir. Eklenti katmanının donanım ve işletim sistemi soyutlamalarıyla ortak tasarım fikrini göstermek için kullanılan açıklayıcı bir sınıflandırmadır.

Donanımdan eklenti katmanına uzanan yedi katmanın şeması

Donanım ve Kernel ​

En alt katmanda işlemci, bellek, depolama ve giriş çıkış denetleyicileri bulunur. Bu katmanın yazılıma verdiği söz komut seti mimarisidir. Komut seti aynı kaldığı sürece üretici iç tasarımı değiştirebilir; farklı önbellek düzenine sahip iki işlemci aynı ikili dosyayı çalıştırır.

Anakart soketleri ve standart sözleşmelerin donanımdaki karşılığı

Kernel donanıma doğrudan erişimi tekeline alır ve bu erişimi sistem çağrıları üzerinden dağıtır. Burada iki ayrı sözleşme vardır ve kararlılık dereceleri farklıdır: kullanıcı alanına bakan sistem çağrısı arayüzü uzun süreli uyumluluk vaat eder, kernel'in iç arayüzleri ise kararlı değildir. Bu ayrım eklenti tasarımı için doğrudan bir derstir. Dışarıya açılan sözleşme ile iç yapı aynı şey değildir ve ikisi aynı hızda değişmemelidir.

Uygulamalar, kernel ve donanım arasındaki erişim akışı

Donanım Soyutlama ve Kütüphaneler ​

Donanım soyutlama katmanı platforma özgü ayrıntıları ortak arayüzlerin arkasında toplar. Windows'ta HAL kesme denetleyicisi, zamanlayıcı ve veri yolu farklılıklarını gizler; Linux tarafında benzer rolü aygıt sürücü modelleri üstlenir.

Anakart üzerindeki denetleyici ve genişleme yuvaları

Aygıt sürücüsü bu katmanın eklentisidir. Kernel belirli bir sürücüyü tanımaz; sürücü tanımlı bir yapı üzerinden kendini kaydeder, bir yaşam döngüsü boyunca çalışır ve kaldırıldığında kaynaklarını bırakır. Bir üst katmanda ise uygulamaların tekrar yazmak istemediği işlevler kütüphanelerde toplanır; paylaşımlı kütüphanelerin çalışma anında yüklenmesi eklenti fikrinin en eski biçimidir.

Windows, Linux ve macOS üzerinde paylaşımlı kütüphane biçimleri

API ve ABI ​

API, bileşenlerin kaynak kodu düzeyinde nasıl haberleşeceğini tanımlar: fonksiyon adları, parametreler, dönüş değerleri, hata davranışı. ABI ise aynı sorunun ikili düzeydeki karşılığıdır: çağrı kuralları, veri hizalaması, yapı düzeni, sembol adlandırma.

API'nin menü benzetmesi ve gerçek hayattan API örnekleri

Aradaki fark pratikte şuna karşılık gelir. API kırılırsa kod derlenmez; hata erkendir ve görünürdür. ABI kırılırsa kod derlenir ama çalışma anında beklenmedik biçimde bozulur. Örnekteki sistemde eklentiler ana uygulamayla birlikte derlendiği için ikili uyumluluk sorunu doğmaz; uyumsuzluk derleme hatasına dönüşür. Eklentilerin ayrı dağıtıldığı sistemlerde ise sürümleme ve uyumluluk politikası yazılı olmak zorundadır.

Uygulama ve Eklenti ​

Uygulama katmanı iş kurallarını yürütür ve alt katmanların yeteneklerini belirli bir amaca göre düzenler. En üstte ise uygulamanın extension API'sine uyarak yeni davranış ekleyen bileşenler bulunur.

Farklı kullanıcı beklentileri karşısında uygulamaların genişleme sorunu

Buradaki bağımlılık yönü dikkat çekicidir. Eklenti ana uygulamayı bilir; ana uygulama eklentiyi bilmez. Bu asimetri, sistem yeni eklentilerle genişlerken çekirdeğin sabit kalmasını mümkün kılar.

Chrome, WordPress ve VS Code eklenti sayıları ile eklenti çalışma adımları

Katmanlar Arasındaki Ortak Fikir ​

SoruKernel modülüUygulama eklentisi
Arayüz nedirKernel modül API'siPlugin arayüzü
Nasıl bulunurModül dizini ve yapılandırmaDizin taraması ve kod üretimi
Nasıl kaydedilirKayıt fonksiyonlarıplugin.Register()
Uyumluluk nasıl denetlenirSürüm ve sembol denetimiDerleme anında arayüz uyumu
Yaşam döngüsüYükleme ve kaldırmainit() ve komut yürütme
Hata sınırıSüreç ve ayrıcalık seviyeleriHata dönüşü, ayrı süreç veya servis

Eklenti sistemi bu nedenle yeni bir fikir değil, işletim sistemi tasarımında uzun süredir kullanılan bir yaklaşımın uygulama düzeyine taşınmış halidir.

Her katmanın eklenebilirlik biçimini gösteren karşılaştırma

Eklenti Sistemi Nedir ​

Eklenti, ana uygulamadan bağımsız geliştirilebilen ancak ana uygulamanın tanımladığı sözleşme üzerinden çalışan bir yazılım bileşenidir. Eklenti yeni bir komut, veri dönüştürücü, doğrulama kuralı, çıktı biçimi veya entegrasyon sağlayabilir.

Kodu ayrı bir dizine koymak tek başına eklenti sistemi oluşturmaz. Tam bir eklenti sistemi en az şu bileşenleri içerir:

BileşenGörev
Extension APIEklentinin sağlaması gereken davranışları ve veri biçimlerini tanımlar.
Keşif mekanizmasıKullanılabilir eklentileri bulur.
Yükleyici veya bağlayıcıEklenti kodunu sisteme dahil eder ve nesnesini oluşturur.
DoğrulayıcıEklentinin sözleşmeye ve desteklenen sürüme uyduğunu denetler.
RegistryKayıtlı eklentileri kimlikleriyle saklar.
Yaşam döngüsü yöneticisiBaşlatma, çalıştırma, durdurma ve kaynak temizleme işlemlerini koordine eder.
Hata ve güvenlik sınırıEklenti hatalarının ve yetkilerinin etkisini sınırlar.

Çalışma Akışı ​

Bir eklentinin ana uygulamaya katılması genellikle aşağıdaki sırayı izler:

  1. Eklenti kaynakları taranır ve kullanılabilir eklentiler belirlenir.
  2. Bulunan eklenti kodu sisteme dahil edilir.
  3. Eklenti nesnesi oluşturulur ve registry'ye kaydedilir.
  4. Kimlik, sürüm ve gerekli metotlar doğrulanır.
  5. Ana uygulama kayıtlı eklentileri kendi akışına bağlar.
  6. Uygulama bir olay veya işlem isteğini ilgili eklentilere iletir.
  7. Hatalar eklenti sınırında kaydedilir ve politika gereği izole edilir.
  8. Uygulama kapanırken eklenti kaynakları serbest bırakılır.

Keşif, bağlama ve çalıştırma ayrı aşamalar olarak ele alınmalıdır. Bu ayrım, eklenti bulunamadığında oluşan hatayla eklenti çalışırken oluşan hatanın farklı biçimde yönetilmesini sağlar.

Extension API Tasarımı ​

Extension API, eklenti sisteminin en önemli parçasıdır. Sözleşme çok dar olursa eklentiler gerekli işlemleri yapamaz; çok geniş olursa uygulamanın iç ayrıntıları dışarı sızar ve API'nin değiştirilmesi zorlaşır.

İyi bir extension API şu özelliklere sahip olmalıdır:

  • Eklentinin kullanabileceği veri ve işlemleri açıkça tanımlamalıdır.
  • Uygulamanın iç nesnelerini doğrudan paylaşmak yerine sınırlı bir bağlam sunmalıdır.
  • Girdi, çıktı ve hata davranışlarını belgelemelidir.
  • Bir sürüm bilgisi içermelidir.
  • Geriye dönük uyumluluk politikasını belirtmelidir.
  • Eklenti kimliğinin benzersiz olmasını sağlamalıdır.

log-analyzer projesinde sözleşme üç metottan oluşur ve internal/plugin/plugin.go dosyasında tanımlanır.

go
package plugin

import "github.com/spf13/cobra"

type Plugin interface {
	Name() string

	Description() string

	Command() *cobra.Command
}

Sözleşmenin bu kadar dar olması bilinçli bir tercihtir. Ana uygulama eklentiden yalnızca üç şey ister: benzersiz bir ad, kullanıcıya gösterilecek bir açıklama ve CLI'ye eklenecek bir komut ağacı. Eklentinin logları nasıl ayrıştırdığı, hangi dosyaları okuduğu veya ağdan ne topladığı çekirdeği ilgilendirmez.

Go'da arayüz uyumu derleme anında denetlenir. Gerekli metotlardan birini sağlamayan bir tip Register() çağrısına geçemez; hata çalışma anında değil, derlemede ortaya çıkar.

Registry ​

Kayıtlı eklentiler internal/plugin/register.go içindeki registry'de tutulur.

go
package plugin

import (
	"fmt"
	"sort"
	"sync"
)

var (
	registry = make(map[string]Plugin)
	mu       sync.Mutex
)

func Register(p Plugin) {
	mu.Lock()
	defer mu.Unlock()

	name := p.Name()
	if name == "" {
		panic("plugin: Register called with empty name")
	}
	if _, exists := registry[name]; exists {
		panic(fmt.Sprintf("plugin: Register called twice for %q", name))
	}
	registry[name] = p
}

func All() []Plugin {
	mu.Lock()
	defer mu.Unlock()

	names := make([]string, 0, len(registry))
	for name := range registry {
		names = append(names, name)
	}
	sort.Strings(names)

	plugins := make([]Plugin, 0, len(names))
	for _, name := range names {
		plugins = append(plugins, registry[name])
	}
	return plugins
}

func Get(name string) (Plugin, bool) {
	mu.Lock()
	defer mu.Unlock()
	p, ok := registry[name]
	return p, ok
}

Üç ayrıntı tasarım açısından önemlidir.

Birincisi doğrulama. Boş ad ve tekrarlanan ad kabul edilmez. İki eklentinin aynı adı kullanması, komut çakışması ve sessizce yanlış eklentinin çalışması anlamına gelir; bu nedenle hata gizlenmez.

İkincisi hatanın zamanı. Register() çağrısı panic ile sonuçlanır. Bu, çalışma anında kullanıcı girdisiyle tetiklenebilecek bir hata olsaydı yanlış bir tercih olurdu. Burada kayıt kümesi derleme anında bellidir ve kayıt program başlarken yapılır; dolayısıyla hata, binary'nin ilk çalıştırıldığı anda ve kesin biçimde ortaya çıkar. Eklentilerin kullanıcı tarafından çalışma anında yüklendiği bir sistemde ise aynı durum hata değeri döndürmeyi gerektirir.

Üçüncüsü sıralama. All(), eklentileri ada göre sıralayarak döndürür. Kayıt sırası init() çalışma sırasına bağlıdır ve bu sıraya güvenilmemelidir. Sıralama sayesinde list komutunun çıktısı ve komutların eklenme sırası her derlemede aynıdır.

Mutex kullanımı da registry'yi ileride eşzamanlı okuma yapan bileşenlere karşı korur.

Kendini Kaydeden Eklenti ​

Her eklenti kendi paketinde sözleşmeyi uygular ve init() içinde kendini kaydeder. Aşağıda log analizi eklentisinin tanımı yer alır.

go
package loganalyzer

import (
	"log-analyzer/internal/plugin"

	"github.com/spf13/cobra"
)

type LogAnalyzerPlugin struct{}

func init() {
	plugin.Register(&LogAnalyzerPlugin{})
}

func (p *LogAnalyzerPlugin) Name() string {
	return "loganalyzer"
}

func (p *LogAnalyzerPlugin) Description() string {
	return "Analyzes system logs and detects security threats"
}

func (p *LogAnalyzerPlugin) Command() *cobra.Command {
	return newRootCommand()
}

Command() metodu eklentinin tüm komut ağacını döndürür. Eklentinin iç yapısı bu noktadan sonra kendi sorumluluğundadır.

go
func newRootCommand() *cobra.Command {
	cmd := &cobra.Command{
		Use:   "loganalyzer",
		Short: "Log analysis and threat detection tool",
	}

	cmd.AddCommand(newStaticCommand())
	cmd.AddCommand(newLiveCommand())
	cmd.AddCommand(newInteractiveCommand())

	return cmd
}

Projedeki üç eklenti aynı sözleşmeyi uygular ancak tamamen farklı işler yapar:

EklentiAçıklamaAlt komutlar
loganalyzerSistem loglarını analiz eder ve güvenlik tehditlerini tespit eder.static, live, interactive
netmetricsScapy ile paket yakalar ve 5G telemetrisini UDP üzerinden iletir.capture, gen5g, collect
streamprocessorSınırlı bellekle gerçek zamanlı akış ayrıştırma ve filtreleme yapar.run, gen

Çekirdek kodda bu adların hiçbiri geçmez.

Keşif ve Kod Üretimi ​

Go'da bir paketin init() fonksiyonunun çalışması için o paketin binary'ye dahil edilmesi gerekir. Hiçbir yerden referans verilmeyen paket derlemeye girmez, dolayısıyla kendini kaydedemez. Projede bu sorun, eklenti dizinlerini tarayan ve boş tanımlayıcılı (blank) import listesi üreten bir araçla çözülür.

Proje Yapısı ​

text
log-analyzer/
├── cmd/
│   └── main.go
├── internal/
│   ├── cli/
│   │   └── root.go
│   └── plugin/
│       ├── plugin.go
│       └── register.go
├── plugins/
│   ├── doc.go
│   ├── plugins_gen.go
│   ├── loganalyzer/
│   ├── netmetrics/
│   └── streamprocessor/
└── tools/
    └── gen/
        └── main.go

Keşif Aracı ​

tools/gen, plugins dizinindeki alt dizinleri tarar. Nokta veya alt çizgi ile başlayan dizinleri atlar, en az bir .go dosyası içerenleri eklenti kabul eder ve sonucu sıralar.

go
func discoverPlugins(root string) ([]string, error) {
	entries, err := os.ReadDir(root)
	if err != nil {
		return nil, fmt.Errorf("read dir %q: %w", root, err)
	}

	var plugins []string
	for _, entry := range entries {
		if !entry.IsDir() {
			continue
		}
		name := entry.Name()
		if strings.HasPrefix(name, ".") || strings.HasPrefix(name, "_") {
			continue
		}

		hasGo, err := dirHasGoFile(filepath.Join(root, name))
		if err != nil {
			return nil, err
		}
		if !hasGo {
			continue
		}

		plugins = append(plugins, name)
	}

	sort.Strings(plugins)
	return plugins, nil
}

Üretilen dosya gofmt ile biçimlendirilip diske yazılır.

go
formatted, err := format.Source(buf.Bytes())
if err != nil {
	return fmt.Errorf("gofmt generated code: %w", err)
}

if err := os.WriteFile(path, formatted, 0644); err != nil {
	return fmt.Errorf("write %q: %w", path, err)
}

Üretim Noktası ​

plugins/doc.go hem toplama noktasını hem de üretim komutunu tanımlar.

go
// Package plugins is the aggregation point for all plugin modules.
// Each plugin lives in its own subdirectory and registers itself via
// init(); this package imports them all via blank imports so their
// init() functions run when the binary starts.
//
// The list of imports is generated automatically by tools/gen. Do not
// edit plugins_gen.go by hand.
package plugins

//go:generate go run ../tools/gen/main.go

Üretilen dosya yalnızca blank import'lardan oluşur.

go
// Code generated by tools/gen; DO NOT EDIT.
// Re-run with: go generate ./...
package plugins

import (
	_ "log-analyzer/plugins/loganalyzer"
	_ "log-analyzer/plugins/netmetrics"
	_ "log-analyzer/plugins/streamprocessor"
)

Alt çizgi ile yapılan import, paketten hiçbir sembol kullanılmadığını ancak paketin yine de binary'ye dahil edilip init() fonksiyonunun çalıştırılmasını istediğimizi belirtir.

Yeni bir eklenti eklemek için plugins/ altına dizin açmak, sözleşmeyi uygulamak ve üretimi tekrar çalıştırmak yeterlidir.

bash
make generate   # plugins/plugins_gen.go dosyasını yeniden üretir
make build      # önce generate çalışır, sonra binary derlenir

İpucu

Yeni eklenti eklendikten sonra kod üretimi çalıştırılmazsa derleme başarılı olur ancak eklenti list çıktısında görünmez. Paket hiçbir yerden import edilmediği için binary'ye girmemiş, dolayısıyla init() hiç çalışmamıştır. Bu sessiz durumu önlemek için build hedefi generate hedefine bağlanmıştır.

Ana Uygulamanın Bağlanması ​

Giriş noktası yalnızca iki şey yapar: eklenti toplama paketini blank import ile dahil eder ve CLI'yi çalıştırır.

go
package main

import (
	"log-analyzer/internal/cli"

	_ "log-analyzer/plugins"
)

func main() {
	cli.Execute()
}

main() çalışmaya başlamadan önce tüm init() fonksiyonları tamamlanır. Bu nedenle CLI kurulurken registry doludur.

go
func bindPlugins(root *cobra.Command) {
	for _, p := range plugin.All() {
		root.AddCommand(p.Command())
	}
}

func newListCommand() *cobra.Command {
	return &cobra.Command{
		Use:   "list",
		Short: "List all registered plugins",
		Run: func(cmd *cobra.Command, args []string) {
			plugins := plugin.All()
			if len(plugins) == 0 {
				fmt.Println("No plugins registered.")
				return
			}
			fmt.Printf("Registered plugins: %d\n\n", len(plugins))
			for _, p := range plugins {
				fmt.Printf("  - %-20s %s\n", p.Name(), p.Description())
			}
		},
	}
}

func Execute() {
	root := newRootCommand()
	root.AddCommand(newListCommand())
	bindPlugins(root)

	if err := root.Execute(); err != nil {
		os.Exit(1)
	}
}

bindPlugins fonksiyonu hiçbir eklenti adını bilmez. Döngü registry üzerinde döner ve her eklentinin kendi komut ağacını kök komuta ekler. Yeni bir eklenti eklendiğinde bu dosyada değişiklik gerekmez.

list komutu ise sistemin kendini anlatmasını sağlar. Kullanıcı hangi yeteneklerin derlendiğini çalıştırma anında görebilir.

Derleme Anı ve Çalışma Anı Keşfi ​

Bu tasarımda keşif derleme anında, kayıt ise süreç başlarken gerçekleşir. Eklentiler çalışma anında dosya sisteminden okunmaz.

AşamaNe olur
Kod üretimiplugins/ taranır, import listesi yazılır
DerlemeEklenti paketleri binary'ye dahil edilir, arayüz uyumu denetlenir
Süreç başlangıcıinit() fonksiyonları Register() çağırır
main()All() ile registry okunur, komutlar bağlanır
Komut yürütmeYalnızca kullanıcının seçtiği eklentinin komutu çalışır

Bu yaklaşımın bedeli, yeni eklenti için yeniden derleme gerekmesidir. Karşılığında üç kazanç elde edilir: uyumsuz eklenti derleme hatası verir, dağıtılan binary tek dosyadır ve çalışma anında güvenilmeyen kod yükleme yüzeyi oluşmaz.

Go'nun standart kütüphanesindeki plugin paketi çalışma anında paylaşımlı nesne yüklemeye izin verir, ancak yalnızca belirli platformlarda çalışır ve ana uygulamayla eklentinin aynı Go sürümü ve aynı bağımlılık sürümleriyle derlenmiş olmasını gerektirir. Pratikte bu kısıtlar, dağıtımı kolaylaştırmak yerine zorlaştırır.

Çalışma anında yükleme yapan dillerde ise keşif ve doğrulama tamamen uygulamanın sorumluluğundadır. Python'da aynı sistem, modülleri tarayan ve her modülden bir fabrika fonksiyonu bekleyen bir yükleyiciyle kurulur.

python
from importlib import import_module
from pkgutil import iter_modules


def discover(package_name: str = "plugins") -> dict[str, object]:
    package = import_module(package_name)
    prefix = f"{package.__name__}."
    registry: dict[str, object] = {}

    for module_info in iter_modules(package.__path__, prefix):
        module = import_module(module_info.name)
        factory = getattr(module, "create_plugin", None)

        if not callable(factory):
            raise RuntimeError(f"{module_info.name}: create_plugin() bulunamadı")

        plugin = factory()
        registry[plugin.plugin_id] = plugin

    return registry

Aradaki fark yalnızca söz dizimi değildir. Go sürümünde derleyicinin yaptığı arayüz denetimi, Python sürümünde elle yazılan doğrulama koduna dönüşür; gerekli metotların varlığı, sürüm uyumu ve kimlik benzersizliği tek tek sınanmak zorundadır.

Uyarı

Her iki yaklaşımda da eklenti, ana uygulamayla aynı süreçte ve aynı yetkilerle çalışır. Derlenmiş bir eklenti de, çalışma anında yüklenmiş bir modül de uygulamanın dosya, ağ ve ortam değişkeni erişimine sahiptir. Güvenilmeyen eklentiler yalnızca hata yakalayarak güvenli hale gelmez; ayrı süreç, işletim sistemi izinleri, kaynak sınırları ve doğrulanmış dağıtım politikası gerekir.

Ayrı Süreçle Yalıtım ​

Projedeki netmetrics eklentisi, aynı süreç kuralının pratik bir istisnasını gösterir. Paket yakalama işi Go içinde değil, Scapy kullanan ayrı bir Python sürecinde yapılır. Betik binary'ye gömülür, geçici dosyaya yazılır ve alt süreç olarak çalıştırılır.

go
//go:embed sniffer.py
var snifferPy []byte
go
ctx, stop := signal.NotifyContext(
	context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()

proc := exec.CommandContext(ctx, python, pyArgs...)
proc.Stdout = os.Stdout
proc.Stderr = os.Stderr
proc.Stdin = os.Stdin

if err := proc.Run(); err != nil {
	if ctx.Err() != nil {
		return nil
	}
	return fmt.Errorf("scapy ajani basarisiz: %w", err)
}

Bu yapı eklenti sınırını süreç sınırına dönüştürür. Yakalama ajanı çökerse ana uygulama ayakta kalır; ham soket için gereken yüksek yetki yalnızca o sürece verilir; iki taraf arasındaki bağ, paylaşılan bellek yerine açıkça tanımlı bir komut satırı ve UDP mesaj biçimidir.

Bedeli de görünürdür. Süreç başlatma maliyeti, Python ve Scapy bağımlılığı, iki tarafta ayrı tutulan sabitler. Betikteki eşik değerinin Go tarafındaki karşılığıyla aynı kalması gerektiği kod içinde not düşülmüştür; bu da süreçler arası sözleşmelerin tıpkı API'ler gibi belgelenmesi gerektiğini gösterir.

SOLID İlkeleri ile İlişki ​

SOLID ilkeleri ve eklenti mimarisiyle doğrudan ilgili olan ikisi

Open Closed Principle ​

Ana uygulama, Plugin arayüzünü uygulayan yeni eklentileri kabul ederek genişler. Yeni bir yetenek için internal/cli/root.go dosyasına koşul eklenmez, switch büyümez, yeni import yazılmaz. Dizin açılır, sözleşme uygulanır, kod üretimi çalıştırılır.

Açık/Kapalı ilkesinin tanımı ve Chrome örneği

Değişime kapalı olmak, çekirdeğin hiçbir zaman değişmeyeceği anlamına gelmez. Sözleşme değişirse, güvenlik düzeltmesi gerekirse veya yaşam döngüsü politikası değişirse çekirdek de güncellenir. İlkenin amacı, olağan genişletmelerin mevcut davranışı sürekli değiştirmesini önlemektir.

Dependency Inversion Principle ​

Üst seviye CLI akışı LogAnalyzerPlugin veya NetMetricsPlugin gibi somut tiplere bağlı değildir. Hem çekirdek hem eklentiler ortak Plugin arayüzüne bağımlıdır. Bağımlılığın yönü somut uygulamadan soyut sözleşmeye çevrilmiştir.

Doğrudan bağımlılık ile sözleşmeye bağımlılığın karşılaştırması

Dikkat edilmesi gereken nokta, sözleşmenin yalnızca metot imzalarından ibaret olmamasıdır. Komut adlarının çakışmaması, hata davranışı ve çıktı biçimi de sözleşmenin parçasıdır.

Single Responsibility Principle ​

Sorumluluklar ayrı dosyalara dağıtılmıştır:

  • internal/plugin/plugin.go sözleşmeyi tanımlar.
  • internal/plugin/register.go kayıt ve doğrulamayı yönetir.
  • tools/gen keşif ve kod üretimini yapar.
  • internal/cli komutları bağlar ve çalıştırır.
  • Her eklenti kendi davranışını uygular.

Bu ayrım, bir eklentinin iş kuralı değiştiğinde registry'nin; keşif yöntemi değiştiğinde ise eklentilerin değiştirilmesini önler.

Interface Segregation Principle ​

Plugin arayüzü üç metottan oluşur ve her eklenti üçünü de gerçekten kullanır. Arayüze ileride yalnızca bazı eklentilerin ihtiyaç duyacağı metotlar eklenmesi, geri kalan eklentileri boş gövdeli metot yazmaya zorlar. Böyle bir ihtiyaç doğduğunda doğru çözüm, ana arayüzü büyütmek yerine isteğe bağlı ikinci bir arayüz tanımlamak ve tip doğrulamasıyla denetlemektir.

Üretim Ortamında Dikkat Edilecek Konular ​

Sözleşme ve Sürümleme ​

Eklentiler ana uygulamayla birlikte derlendiği sürece sürüm uyumsuzluğu derleme hatasıdır. Eklentilerin ayrı depolarda geliştirildiği veya ayrı dağıtıldığı bir aşamaya geçilirse arayüze sürüm bilgisi eklenmesi ve uyumsuz eklentinin kayıt aşamasında açık bir mesajla reddedilmesi gerekir. Büyük sözleşme değişikliklerinde bir geçiş dönemi veya uyumluluk adaptörü gerekebilir.

Hata Yalıtımı ​

Bir eklentinin hatası diğerlerinin çalışmasını durdurmamalıdır. Komut tabanlı bu tasarımda aynı anda tek bir eklenti çalıştığı için etki alanı doğal olarak dardır. Eklentilerin aynı anda çağrıldığı bir modele geçilirse her çağrı kendi hata sınırına alınmalıdır. Süre aşımı, aşırı bellek kullanımı veya süreç çökmesi gibi sorunlar aynı süreçte tam olarak yalıtılamaz; kritik bileşenler netmetrics örneğindeki gibi ayrı süreçte çalıştırılabilir.

Yükleme Sırası ve Bağımlılıklar ​

init() fonksiyonlarının çalışma sırası paket bağımlılıklarına göre belirlenir ve eklentiler arasında garanti edilmez. All() fonksiyonunun ada göre sıralama yapması bu belirsizliği dışarıya yansıtmaz. Eklentiler birbirine bağımlı hale gelirse bu ilişki açıkça tanımlanmalı, çevrimsel bağımlılıklar reddedilmeli ve ortak değiştirilebilir durum paylaşımından kaçınılmalıdır.

Yapılandırma ​

Her eklenti yalnızca kendi yapılandırma bölümüne erişmelidir. Ana uygulamanın tüm ayar nesnesini paylaşmak, eklentileri çekirdeğin ayrıntılarına bağımlı hale getirir ve yetki sınırlarını genişletir.

Gözlemlenebilirlik ​

Kayıtlı eklenti adı ve sürümü, başlatma süresi, çalışma süresi, hata sayısı ve devre dışı bırakma nedeni kaydedilmelidir. Log ve metriklerde eklenti adının bulunması, hatanın kaynağını ayırmayı kolaylaştırır. list komutu bu bilginin en basit biçimidir.

Test Edilebilirlik ​

Sözleşme açıkça tanımlandığında eklentiler ana uygulama çalıştırılmadan test edilebilir. Registry tarafında ise boş ad, tekrarlanan ad ve sıralama davranışının da sınanması gerekir. Bu yollar üretimde en çok sorun çıkaran noktalardır.

Güvenlik ​

Eklenti kodunun kaynağı, bütünlüğü ve yetkileri doğrulanmalıdır. İmzalı paketler, izin listeleri ve bağımlılık taraması tedarik zinciri riskini azaltabilir. Hassas işlemler, eklentiye geniş bir uygulama nesnesi vermek yerine dar yetkili servis arayüzleri üzerinden sunulmalıdır. Yüksek yetki gerektiren işler, ayrı süreçte ve yalnızca gereken izinlerle çalıştırılmalıdır.

Sonuç ​

Katmanlı tasarımın temel fikri sistemin her düzeyinde aynıdır: değişmesi beklenen kısmı kararlı bir arayüzün arkasına almak. Komut seti işlemci tasarımını, sistem çağrısı arayüzü kernel içini, sürücü modeli donanım ayrıntısını, extension API ise uygulama çekirdeğini korur.

Eklenti sisteminin temeli bu nedenle kodu dizinlere bölmek değil, ana uygulama ile eklentiler arasında kararlı ve sınırlı bir sözleşme kurmaktır. log-analyzer örneğinde sözleşme üç metottan ibarettir; keşif bir kod üretim aracıyla, kayıt init() fonksiyonlarıyla, bağlama ise registry üzerinde dönen tek bir döngüyle yapılır. Çekirdek hiçbir eklentinin adını bilmez.

Open/Closed sistemin yeni eklentilerle genişletilmesini, Dependency Inversion ana uygulamanın somut tipler yerine sözleşmeye bağlanmasını, Single Responsibility ise keşif, kayıt ve eklenti davranışlarının ayrı kalmasını destekler. Bu ilkeler sayesinde log analizi, ağ telemetrisi ve akış işleme gibi birbirinden tamamen farklı üç yetenek, tek bir çekirdeğin etrafında birbirini etkilemeden çalışabilir.


Kaynakça ​

Eklenti(Plugin) Sistemi İçin Kaynakça