Documentation
¶
Overview ¶
Package record writes session streams in asciicast v2 format.
Format — ilk satır başlık, sonrası olay satırları:
{"version":2,"width":80,"height":24,"timestamp":1723465329,"env":{"TERM":"xterm-256color"}}
[0.248848,"o","�[?1034h"]
[2.143881,"i","ls\r"]
[3.002100,"r","120x30"]
Kayıt için terminal emülasyonu GEREKMEZ (Ek C.7): bayt akışını olduğu gibi saklıyoruz, emülasyonu oynatıcı yapıyor.
⚠️ SADAKATİN SINIRI: asciicast v2 veriyi JSON DİZESİ olarak saklar ve JSON dizesi rastgele bayt tutamaz. encoding/json geçersiz her UTF-8 baytını U+FFFD (replacement character) ile değiştirir. Yani hedefte `cat /bin/ls` çalıştıran bir oturum BİREBİR kaydedilmez.
Bu formatın seçilmesinin bedeli ve bilerek kabul ediliyor: kayıtların amacı bir insanın oturumu izlemesi, ikili veriyi yeniden üretmek değil. Ama "ham baytları saklıyoruz" demek yanlış olurdu ve bir olay incelemesinde yanlış beklenti üretirdi. (Ölçüldü: TestWriterReplacesInvalidUTF8Bytes bu davranışı sabitliyor.)
Index ¶
- Variables
- func FreeSpace(dir string) (uint64, error)
- func NewSessionID() (string, error)
- func Usage(dir string) (files int, bytes int64, skipped int, err error)
- type Archived
- type PruneResult
- type Pruner
- func (p *Pruner) RunOnce(ctx context.Context) PruneResult
- func (p *Pruner) Start(ctx context.Context)
- func (p *Pruner) WithArchive(a Archived) *Pruner
- func (p *Pruner) WithAuditLog(onDeleted func(context.Context, []string)) *Pruner
- func (p *Pruner) WithOpenSessions(open func() map[string]bool) *Pruner
- type Store
- type Writer
- func (w *Writer) Close() (err error)
- func (w *Writer) Err() error
- func (w *Writer) Input(b []byte) error
- func (w *Writer) InputStream() io.Writer
- func (w *Writer) OnFailure(fn func(error))
- func (w *Writer) Output(b []byte) error
- func (w *Writer) OutputStream() io.Writer
- func (w *Writer) Resize(cols, rows int) error
Constants ¶
This section is empty.
Variables ¶
var ( // ErrNotRecorded: oturumun kaydı hiç olmamış (recording_path boş). ErrNotRecorded = errors.New("record: session has no recording") // ErrOutsideRoot: saklanan yol kayıt kökünün DIŞINDA. // // Bu bir "bulunamadı" değil, bir REDDETME: kökün dışını gösteren bir // yol ya recording.dir değişmiş demektir ya da veritabanına oraya ait // olmayan bir değer girmiş demektir. İkisi de sessizce dosya // açılacak durumlar değil. ErrOutsideRoot = errors.New("record: stored path is outside the recordings root") )
Kayıt okuma yolundaki hatalar.
var ErrDiskLow = errors.New("record: not enough free space for a new recording")
ErrDiskLow, kayıt için ayrılan yerde eşiğin altında boş alan kaldı.
Functions ¶
func FreeSpace ¶
* FreeSpace, kayıt dizininin bulunduğu dosya sisteminde kalan bayt. * * ⚠️ AYRILMIŞ BLOKLAR SAYILMIYOR (Bavail, Bfree değil). root olmayan * bir süreç ayrılmış bloklara yazamıyor; onları "boş" saymak, gerçekte * dolmuş bir diskte hâlâ yer var demek olurdu.
func NewSessionID ¶
NewSessionID returns a random, filesystem-safe session identifier.
func Usage ¶
* Usage, kayıt dizininin toplam boyutu ve dosya sayısı. * * ⚠️ GÖRÜNÜRLÜK BUDAMADAN ÖNCE GELİR. Operatör ne kadar yer * kapladığını göremeden bir saklama süresi seçemez — ve göremediği * için de seçmez. Varsayılanı "hiç silme" olan bir ayarın işe * yaraması, bu sayının bir yerde yazmasına bağlı.
Types ¶
type Archived ¶
type Archived interface {
// ArchivedIDs, verilenlerden hangilerinin DOĞRULANMIŞ şekilde
// arşivlendiğini döner. Kümede olmayan her kimlik "silinemez".
ArchivedIDs(ctx context.Context, ids []string) (map[string]bool, error)
}
* Prune, cutoff'tan ESKİ kayıtları siler. * * ⚠️ DEĞİŞTİRİLME ZAMANINA BAKIYOR, dizin adına değil. Dizin adı * (2026-08-31) kaydın AÇILDIĞI günü söylüyor; uzun süren bir oturum * ertesi güne sarkabiliyor. Ada bakan bir budayıcı, ertesi gün hâlâ * yazılan o dosyayı yaşlı sanıp silerdi. * * ⚠️ AMA mtime YALNIZCA AKTİF YAZILANI KORUR. Çıktı üretmeyen boşta * bir oturumun mtime'ı da eskir; onu openIDs (açık oturum kümesi) * koruyor, mtime değil. * * ⚠️ SIFIR SÜRE HİÇBİR ŞEY SİLMİYOR. Bu fonksiyonun yanlış çağrılması * bütün denetim kaydını silmek demek; "ayar verilmemiş" hâlinin * "hepsini sil" olarak okunması, olabilecek en pahalı sıfır değeri * olurdu.
* Archived, "bu kayıtlar başka bir yerde güvende mi" sorusunu soran * taraf. Uygulaması internal/archive'da; burada yalnızca ARAYÜZ var. * * ⚠️ ARAYÜZ TÜKETİCİ TARAFINDA TANIMLI, ÇÜNKÜ record PAKETİ HİÇBİR * PROJE PAKETİNİ IMPORT ETMİYOR — yalnızca standart kütüphane. Buraya * store'u sokmak, kayıt yazma yolunu veritabanına bağımlı hâle * getirirdi; oysa o yolun tek işi diske yazmak ve hiçbir dış sisteme * bağlı olmaması, "kayıt tutulamıyorsa oturum reddedilir" kuralının * anlamlı kalmasının şartı.
type PruneResult ¶
type PruneResult struct {
Files int
Bytes int64
// Dirs, tamamen boşalıp kaldırılan gün dizinleri.
Dirs int
/*
* ⚠️ TUTULANLAR DA SAYILIYOR, ve gerekçesi silinenler kadar önemli.
*
* Arşivlenmemiş bir kayıt saklama süresi dolsa bile silinmiyor.
* Doğru davranış bu — ama görünmezse, disk yavaşça doluyor ve
* operatör bir gün "oturumlar reddediliyor" diye uyanıyor. Sayılar
* her koşuda loglanıyor ki sıkışma günler önce görünsün.
*/
KeptUnarchived int
KeptBytes int64
// Unknown, adından oturum kimliği çıkmayan dosyalar: yetim
// kayıtlar ya da dizine düşmüş yabancılar. Silinmiyor, sayılıyor.
Unknown int
// Deleted, silinen oturumların kimlikleri. Çağıran bunları denetim
// defterine yazıyor: kanıtın kaybolması da bir olay.
Deleted []string
}
PruneResult, bir budama koşusunun sonucu.
func Prune ¶
func Prune(ctx context.Context, dir string, keepFor time.Duration, now time.Time, archived Archived, openIDs map[string]bool) (PruneResult, error)
* Prune, saklama süresi dolan kayıtları siler. * * archived nil ise arşivleme kapalıdır ve davranış eskisiyle aynı: * yaşı geçen dosya silinir. * * ⚠️ archived DOLUYSA KAPI VARSAYILAN OLARAK KAPALI. Yalnızca * "evet, doğrulanmış şekilde arşivlendi" cevabı silmeye izin veriyor. * Sorgu hata verirse KOŞU İPTAL EDİLİYOR — hiçbir şey silinmeden. * Bu, bu dosyadaki diğer hata davranışlarının TERSİ (CheckSpace * ölçemediğinde nil dönüyor, dizin okunamadığında continue ediliyor) * ve fark bilinçli: orada bedel bir oturumun reddedilmemesi, burada * bedel denetim kanıtının yok olması.
type Pruner ¶
type Pruner struct {
// contains filtered or unexported fields
}
Pruner, eski kayıtları periyodik olarak siler.
func (*Pruner) RunOnce ¶
func (p *Pruner) RunOnce(ctx context.Context) PruneResult
RunOnce, tek bir budama koşusu.
func (*Pruner) Start ¶
* Start, döngüyü çalıştırır. ctx bitene kadar dönmüyor. * * ⚠️ AÇILIŞTA HEMEN BİR KOŞU YAPMIYOR. Bir saat beklemek, yanlış * yapılandırılmış bir sürenin (ör. "1h" yerine "1s") ilk saniyede * bütün arşivi silmesine karşı verilmiş bir pencere: operatör * açılıştaki log satırını görüp durdurabiliyor.
func (*Pruner) WithArchive ¶
* WithArchive, budayıcıya arşiv kapısını takar. * * ⚠️ Bunu takmadan arşivleme açmak, henüz yüklenmemiş kayıtların * silinmesi demek. serve.go ikisini BİRLİKTE kuruyor; ayrı ayrı * yapılandırılabilir olsalardı biri unutulabilirdi. * * ⚠️ SİLME KAYDINA DOKUNMUYOR: o artık WithAuditLog ile, arşivlemeden * BAĞIMSIZ olarak takılıyor. Denetim yazımını bu kapıya bağlamak, tam * da arşivleme kapalıyken (varsayılan) sessizce kanıt silinmesine yol * açıyordu.
func (*Pruner) WithAuditLog ¶
* WithAuditLog, silinen kayıtların denetim defterine yazılmasını sağlar * — arşivleme AÇIK OLMASA DA. * * ⚠️ NEDEN AYRI: silme geri çağrısı eskiden yalnızca WithArchive ile, * yani arşivleyici varsa takılıyordu. Arşivleme varsayılan KAPALI * olduğu için sıradan kurulumda budayıcı kanıt siliyor ve admin_log'a * hiçbir şey yazmıyordu — panel ise kayıp bir kaydın sebebini "admin * log söyler" diye anlatıyor. Denetim yazımı arşivlemeden bağımsız * olmalı.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store owns the on-disk layout of recordings.
func (*Store) CheckSpace ¶
* CheckSpace, yeni bir kayıt açmadan önce yer var mı diye bakar. * * ⚠️ SIFIR EŞİK "KAPALI" DEMEK, "her zaman reddet" değil. Ayarın * yokluğu bir kapıyı kapatmamalı. * * ⚠️ ÖLÇÜM YAPILAMIYORSA GEÇİYORUZ. Statfs'in başarısız olması, * diskin dolu olduğunu göstermez — dosya sistemi hakkında hiçbir şey * bilmediğimizi gösterir. Bilmemeyi "dolu" saymak, ölçüm hatasını * kesintiye çevirirdi; asıl koruma zaten yazmanın kendisinin * başarısız olması.
func (*Store) Open ¶
Open, bir oturumun kayıt dosyasını okumak için açar.
⚠️ NEDEN storedPath'i DOĞRUDAN os.Open'a VERMİYORUZ: sessions. recording_path bir VERİTABANI SÜTUNU. Bir veritabanı değerini dosya yolu olarak kullanmak, veritabanına yazabilen her yolu (başka bir yerdeki enjeksiyon, operatörün elle UPDATE'i, geri yüklenen bir dump, ileride eklenecek bir içe aktarma özelliği) yetkili bir admin oturumu üzerinden KEYFİ DOSYA OKUMAYA çevirir. Bariz hedef ca.key_file.
Bu yüzden iki bağımsız kontrol var: sessionID kimlik doğrulaması (Create ile aynı desen) ve yolun kayıt kökünün altında kaldığının ispatı. İkisi de geçmeden dosya açılmıyor.
type Writer ¶
type Writer struct {
// contains filtered or unexported fields
}
Writer serializes a session into asciicast v2.
⚠️ Eşzamanlılık: Output ve Input FARKLI goroutine'lerden gelir (broker'ın beş akışını hatırla). mu olmadan iki olay satırı iç içe geçer ve dosya bozulur.
KİLİT DİSİPLİNİ — üç katman:
dış (Output/Input/Resize/Close) → mu'yu BURADA al orta (writeStream/flush) → mu tutuluyor varsayar alt (writeEvent) → mu tutuluyor varsayar
sync.Mutex reentrant DEĞİLDİR: iç katmanlar kilit alsaydı Close→flush→ writeEvent zinciri kendi kendini kilitlerdi. Tek giriş noktası dış katman olduğu için disiplin tek yerde toplanıyor.
func (*Writer) Input ¶
Input records user→target bytes as an "i" event.
⚠️ Girdi kaydı VARSAYILAN KAPALI olmalı (config record_input: false): kullanıcının yazdığı her şey girdidir — sudo parolası dahil. Bu metod yalnızca açıkça istendiğinde çağrılır; kararı çağıran verir.
func (*Writer) InputStream ¶
InputStream is the "i" counterpart of OutputStream.
func (*Writer) OnFailure ¶
OnFailure, kayıt yazımı ilk kez başarısız olduğunda çağrılacak fonksiyonu kurar.
⚠️ NEDEN VAR: bu kanca eklenene kadar oturum ortasındaki yazma hataları YUTULUYORDU — akış devam ediyor, oturum kayıtsız sürüyor ve hatanın tek izi kapanışta bir log satırı oluyordu. Açılışta "kayıt açılamazsa oturum reddedilir" (proxy.Open ErrUnavailable döner) denirken, açıldıktan sonra aynı arıza sessizce kabul ediliyordu: politika yarım uygulanıyordu.
Bir saldırgan diski doldurarak ya da kayıt dosyasını kaldırarak KENDİ oturumunu ve o an açık DİĞER oturumları kayıtsız hâle getirebiliyordu — ürünü kayıt olan bir sistemde bu, denetimi kapatmanın yolu demek.
Dinlemeye başlamadan ÖNCE çağrılmalı.
func (*Writer) OutputStream ¶
OutputStream returns an io.Writer recording everything as "o" events.
func (*Writer) Resize ¶
* Resize records a terminal size change as an "r" event ("120x30"). * * ⚠️ HATASI KAYIT ANAHTARINA BAĞLI — VE DEĞİLDİ. noteFailure yalnızca * akış sarmalayıcılarından (OutputWriter/InputWriter) çağrılıyordu; * Resize bir Writer metodu ve hatası hiçbir zaman oraya ulaşmıyordu. * Sonuç, "kaydedilemeyen oturum geçmez" kuralında küçük ama gerçek bir * delik: pencere boyutu diske yazılamıyorken oturum akmaya devam * ediyor, kayıt sessizce eksiliyor ve broker yalnızca log'a bir satır * bırakıyordu. * * ⚠️ noteFailure KİLİDİN DIŞINDA çağrılıyor: kendisi w.mu'yu alıyor ve * `defer w.mu.Unlock()` altında çağırmak kilitlenme olurdu.