Compare commits

..

1 Commits

Author SHA1 Message Date
serr 975e7e8a4c add client cache, add page struct 2025-04-23 12:17:20 +03:00
38 changed files with 153 additions and 441 deletions

View File

@ -1,17 +1,13 @@
::selection {
background: purple;
color: #fff;
}
/* Design idea from here https://mo.rijndael.cc/ */
body {
font-family: -apple-system,system-ui,blinkmacsystemfont,Segoe UI,roboto,oxygen,ubuntu,Helvetica Neue,arial,sans-serif;
display: flex;
flex-wrap: wrap;
margin: 0;
align-items: flex-start;
padding: 10px;
background-image: url("/assets/pic/star.gif");
background-color: #000;
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHdpZHRoPSc0MCcgaGVpZ2h0PSc0MCc+PHJlY3QgZmlsbD0nIzkxZWFlNCcgeD0nMCcgeT0nMCcgd2lkdGg9JzIwcHgnIGhlaWdodD0nMjBweCcvPjxyZWN0IGZpbGw9JyM4NmE4ZTcnIHg9JzAnIHk9JzIwJyB3aWR0aD0nMjBweCcgaGVpZ2h0PScyMHB4Jy8+PHJlY3QgZmlsbD0nIzkxZWFlNCcgeD0nMjAnIHk9JzIwJyB3aWR0aD0nMjBweCcgaGVpZ2h0PScyMHB4Jy8+PHJlY3QgZmlsbD0nIzg2YThlNycgeD0nMjAnIHk9JzAnIHdpZHRoPScyMHB4JyBoZWlnaHQ9JzIwcHgnLz48L3N2Zz4=");
background-repeat: repeat;
}
header, main, footer {
@ -31,114 +27,32 @@ main {
div {
text-align: left;
background-color: #b1cfd8;
box-shadow: 0 0 3px 3px rgba(135, 182, 196, 0.95);
background-color: white;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
box-sizing: border-box;
border: 1px solid;
border-radius: 10px;
width: 100%;
padding: 0 20px;
overflow: hidden;
}
h1 {
text-align: center;
padding: 30px;
position: relative;
}
h1::before, h1::after {
content: "";
position: absolute;
left: 0;
right: 0;
height: 10px;
background: url('data:image/svg+xml;utf8,<svg viewBox="0 0 100 10" xmlns="http://www.w3.org/2000/svg"><path d="M0,5 Q25,10 50,5 T100,5" fill="none" stroke="%236a8498" stroke-width="2"/></svg>');
}
h1::before { top: 0; }
h1::after { bottom: 0; }
a {
background-color: #77abbb;
color: black;
text-decoration: none;
border-radius: 5px;
padding: 0 5px;
transition: background-color 0.1s ease;
box-sizing: border-box;
border-top: 1px solid;
border-bottom: 1px solid;
}
a:hover {
background-color: #778bbb;
.count {
display: flex;
justify-content: center;
}
pre {
padding: 10px;
background: #77abbb;
border: 1px solid #000;
overflow-x: auto;
white-space: pre;
word-wrap: normal;
width: 0;
min-width: calc(100% - 22px);
.count img {
height: 75px;
}
details > summary {
cursor: pointer;
}
details[open] > summary {
cursor: pointer;
}
.rotating-pic-container {
width: 100%;
height: 100%;
overflow: hidden;
}
.rotating-pic {
width: 100%;
height: 100%;
animation: swing 1.5s ease-in-out infinite alternate;
}
@keyframes swing {
0% { transform: rotate(-10deg); }
100% { transform: rotate(10deg); }
}
.pulse-container {
text-align: center;
}
.pulse-text {
font-weight: bold;
color: purple;
animation: scalePulse 2s infinite ease-in-out;
}
@keyframes scalePulse {
0%, 100% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
}
@media (max-width: 800px) {
a:hover {
background-color: #77abbb;
}
@media (max-width: 1200px) {
header, footer, main {
flex: 1 100%;
}
details > summary {
cursor: default;
}
details[open] > summary {
cursor: default;
}
}

BIN
assets/pic/0.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
assets/pic/1.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

BIN
assets/pic/2.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

BIN
assets/pic/3.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
assets/pic/4.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

BIN
assets/pic/5.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

BIN
assets/pic/6.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

BIN
assets/pic/7.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

BIN
assets/pic/8.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
assets/pic/9.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

BIN
assets/pic/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
assets/pic/favicon.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 488 B

BIN
assets/pic/footer.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

BIN
assets/pic/header.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

2
eye.sh
View File

@ -3,7 +3,7 @@ stty -echoctl # Отключает вывод управляющих симво
# НАСТРОЙКА СКРИПТА ТУТ ###########################################################
DURATION=1 # Задержка между проверками в секундах
WATCH_TARGETS=("assets" "mvc" "posts" "tools" "main.go" "config.json") # Массив целей для наблюдения (директории и файлы)
WATCH_TARGETS=("assets" "mvc" "posts" "tools" "main.go") # Массив целей для наблюдения (директории и файлы)
BINARY_PATH="./main" # Путь до бинарного файла
BUILD_CMD="go build -o $BINARY_PATH main.go" # Команда для сборки
###################################################################################

30
main.go
View File

@ -8,7 +8,6 @@ import (
"main/mvc/models"
"main/tools"
"net/http"
"time"
)
func main() {
@ -26,19 +25,6 @@ func main() {
// Настройка маршрутов
router := setupRoutes(app)
// Запуск тикеров
{
// Обновление последнего прослушанного трека ластфм
tools.Ticker(func() {
var lastTrack string
if lastTrack, err = models.LastFMGetLastTrack(app.Cfg.LastFMUsername, app.Cfg.LastFMToken); err != nil {
log.Println(err)
return
}
app.LastfmLastTrack = lastTrack
}, time.Second*app.Cfg.LastFMUpdateInterval)
}
// Запуск сервера
if ok, err := tools.IsIPInUse(app.Cfg.ServerIP); err != nil {
log.Fatal(err)
@ -65,17 +51,11 @@ func setupRoutes(app *models.App) *http.ServeMux {
router.Handle("/", m(controllers_pages.MainPageHandler(app)))
// Обработка страницы со списком постов
router.Handle("/posts/", m(controllers_pages.PostsPageHandler(app)))
}
// Обработка страничек постов
for key := range app.Posts {
postLink := string(key)
router.Handle(postLink, m(controllers_pages.PostPageHandler(app)))
}
// Api
{
router.Handle("/api/lastfm", m(controllers.LastFMHandler(app)))
// Обработка страничек постов
for key := range app.Posts {
postLink := string(key)
router.Handle(postLink, m(controllers_pages.PostPageHandler(app)))
}
}
return router

View File

@ -1,7 +1,6 @@
package controllers_pages
import (
"fmt"
"log"
"main/mvc/models"
@ -39,25 +38,35 @@ func MainPageHandler(app *models.App) http.HandlerFunc {
}
// Страничка рендерится только если ее нет в кэше
pageData, ok := app.PagesCache.Get(models_pages.MainPageTmplName)
page, ok := app.PagesCache.Get(models_pages.MainPageTmplName)
if !ok {
pageData, err = models_pages.RenderMainPage(app.Templates, app.Version)
pageData, err := models_pages.RenderMainPage(app.Templates, app.Version)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
app.PagesCache.Set(models_pages.MainPageTmplName, pageData)
ETag := models.GenerateETag(pageData)
page = &models.Page{Data: pageData, ETag: ETag}
app.PagesCache.Set(models_pages.MainPageTmplName, page)
}
sendMainPage(w, pageData)
sendMainPage(r, w, page)
})
}
// Отправляет страницу
func sendMainPage(w http.ResponseWriter, data []byte) {
func sendMainPage(r *http.Request, w http.ResponseWriter, page *models.Page) {
w.Header().Set("Cache-Control", "public, max-age=31536000")
w.Header().Set("ETag", page.ETag)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK)
w.Write(data)
if match := r.Header.Get("If-None-Match"); match == page.ETag {
w.WriteHeader(http.StatusNotModified)
return
}
w.Write(page.Data)
}
// Ответ на метод COUNT
@ -71,7 +80,7 @@ func sendCount(w http.ResponseWriter, data []byte) {
func sendLove(w http.ResponseWriter) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, "13.01.2005\n")
w.Write([]byte("13.01.2005\n"))
}
// Ответ на метод LIMINAL
@ -79,5 +88,6 @@ func sendLiminal(w http.ResponseWriter) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusOK)
text := "If you're not careful and you slip out of reality in the wrong place, you'll end up in the Backstage, where there's nothing but the stench of old damp carpet, yellow-colored madness, the endless unbearable hum of fluorescent lights, and roughly six hundred million square miles of randomly arranged empty rooms.\n"
fmt.Fprint(w, text)
w.Write([]byte(text))
}

View File

@ -10,8 +10,6 @@ import (
// Обработчик главной страницы
func PostPageHandler(app *models.App) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var err error
postLink := r.URL.Path
// Ссылки на посты имеют вид postLink = /link/, а если прилетело что то типо /link/123123,
@ -22,26 +20,33 @@ func PostPageHandler(app *models.App) http.HandlerFunc {
}
// Страничка рендерится только если ее нет в кэше
pageData, ok := app.PagesCache.Get(postLink)
page, ok := app.PagesCache.Get(postLink)
if !ok {
post := app.Posts[models_pages.PostLink(postLink)]
pageData, err = post.RenderPostPage(app.Templates, app.Version)
pageData, err := models_pages.RenderPostPage(app.Templates, app.Version, post.Data)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
app.PagesCache.Set(postLink, pageData)
ETag := models.GenerateETag(pageData)
page = &models.Page{Data: pageData, ETag: ETag}
app.PagesCache.Set(postLink, page)
}
sendPostPage(w, pageData)
sendPostPage(r, w, page)
})
}
// Отправляет страницу
func sendPostPage(w http.ResponseWriter, data []byte) {
func sendPostPage(r *http.Request, w http.ResponseWriter, page *models.Page) {
w.Header().Set("Cache-Control", "public, max-age=31536000")
w.Header().Set("ETag", page.ETag)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK)
w.Write(data)
if match := r.Header.Get("If-None-Match"); match == page.ETag {
w.WriteHeader(http.StatusNotModified)
return
}
w.Write(page.Data)
}

View File

@ -9,26 +9,33 @@ import (
// Обработчик главной страницы
func PostsPageHandler(app *models.App) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var err error
// Страничка рендерится только если ее нет в кэше
pageData, ok := app.PagesCache.Get(models_pages.PostsPageTmplName)
page, ok := app.PagesCache.Get(models_pages.PostsPageTmplName)
if !ok {
pageData, err = app.Posts.RenderPostsPage(app.Templates, app.Version)
pageData, err := app.Posts.RenderPostsPage(app.Templates, app.Version)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
app.PagesCache.Set(models_pages.PostsPageTmplName, pageData)
ETag := models.GenerateETag(pageData)
page = &models.Page{Data: pageData, ETag: ETag}
app.PagesCache.Set(models_pages.PostsPageTmplName, page)
}
sendPostsPage(w, pageData)
sendPostsPage(r, w, page)
})
}
// Отправляет страницу
func sendPostsPage(w http.ResponseWriter, data []byte) {
func sendPostsPage(r *http.Request, w http.ResponseWriter, page *models.Page) {
w.Header().Set("Cache-Control", "public, max-age=31536000")
w.Header().Set("ETag", page.ETag)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK)
w.Write(data)
if match := r.Header.Get("If-None-Match"); match == page.ETag {
w.WriteHeader(http.StatusNotModified)
return
}
w.Write(page.Data)
}

View File

@ -1,21 +0,0 @@
package controllers
import (
"fmt"
"main/mvc/models"
"net/http"
)
// Обработчик главной страницы
func LastFMHandler(app *models.App) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
sendLastFM(w, app.LastfmLastTrack)
})
}
// Отправляет страницу
func sendLastFM(w http.ResponseWriter, data string) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, data)
}

View File

@ -7,12 +7,11 @@ import (
)
type App struct {
Cfg *Config // Сонфиг
Posts models_pages.Posts // Посты
Templates *template.Template // Шаблоны страниц
PagesCache *Cache // Кэш (отрендеренные странички)
LastfmLastTrack string // Последний трек, полученный с ластфм
Version int64 // Время запуска
Cfg *Config // Сонфиг
Posts models_pages.Posts // Посты
Templates *template.Template // Шаблоны страниц
PagesCache *Cache // Кэш (отрендеренные странички)
Version int64 // Время запуска
}
// Инициализирует приложение
@ -37,7 +36,5 @@ func InitApp() (*App, error) {
}
// Инициализация кэша
app.PagesCache = initCache()
// Строка по умолчанию для последнего прослушанного трека
app.LastfmLastTrack = "None"
return app, nil
}

View File

@ -1,25 +1,40 @@
package models
import "sync"
import (
"crypto/md5"
"encoding/hex"
"fmt"
"sync"
)
type Page struct {
Data []byte
ETag string
}
type Cache struct {
Data map[string][]byte
Data map[string]*Page
Mu sync.RWMutex
}
func initCache() *Cache {
return &Cache{Data: make(map[string][]byte)}
return &Cache{Data: make(map[string]*Page)}
}
func (c *Cache) Get(key string) ([]byte, bool) {
func (c *Cache) Get(key string) (*Page, bool) {
c.Mu.RLock()
pageData, ok := c.Data[key]
page, ok := c.Data[key]
c.Mu.RUnlock()
return pageData, ok
return page, ok
}
func (c *Cache) Set(key string, data []byte) {
func (c *Cache) Set(key string, page *Page) {
c.Mu.Lock()
c.Data[key] = data
c.Data[key] = page
c.Mu.Unlock()
}
func GenerateETag(data []byte) string {
hash := md5.Sum(data)
return fmt.Sprintf(`"%s"`, hex.EncodeToString(hash[:]))
}

View File

@ -3,7 +3,6 @@ package models
import (
"encoding/json"
"os"
"time"
)
const (
@ -11,19 +10,16 @@ const (
)
type Config struct {
PostsDir string
AssetsDir string
TemplatesDir string
TemplatesExt string
LocalIP string
LocalPort string
ServerIP string
ServerPort string
ServerDomain string
Port string
LastFMUsername string
LastFMToken string
LastFMUpdateInterval time.Duration
PostsDir string
AssetsDir string
TemplatesDir string
TemplatesExt string
LocalIP string
LocalPort string
ServerIP string
ServerPort string
ServerDomain string
Port string
}
func loadConfig(configPath string) (*Config, error) {

View File

@ -1,63 +0,0 @@
package models
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
)
type LastFMTrack struct {
Name string `json:"name"`
Artist struct {
Name string `json:"#text"`
} `json:"artist"`
Album struct {
Name string `json:"#text"`
} `json:"album"`
Date struct {
Unix string `json:"#text"`
} `json:"date"`
}
type LastFMResponse struct {
RecentTracks struct {
Tracks []LastFMTrack `json:"track"`
} `json:"recenttracks"`
}
func LastFMGetRecentTracks(username, apiKey string) (*LastFMResponse, error) {
url := fmt.Sprintf("https://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user=%s&api_key=%s&format=json", username, apiKey)
resp, err := http.Get(url)
if err != nil {
return nil, fmt.Errorf("lastfm get resp err: %v", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("lastfm read resp body err: %v", err)
}
var lastFMResponse LastFMResponse
if err := json.Unmarshal(body, &lastFMResponse); err != nil {
return nil, fmt.Errorf("lastfm unmarshal err: %v", err)
}
return &lastFMResponse, nil
}
func LastFMGetLastTrack(username, apiKey string) (string, error) {
resp, err := LastFMGetRecentTracks(username, apiKey)
if err != nil {
return "", err
}
var data string
if len(resp.RecentTracks.Tracks) > 0 {
track := resp.RecentTracks.Tracks[0]
data = fmt.Sprintf("%s - %s", track.Name, track.Artist.Name)
data = strings.ReplaceAll(data, "🅴", "[E]")
} else {
return "", fmt.Errorf("len(resp.RecentTracks.Tracks) <= 0")
}
return data, nil
}

View File

@ -20,14 +20,13 @@ type Post struct {
Timestamp int64
}
func (p *Post) RenderPostPage(templates *template.Template, version int64) ([]byte, error) {
func RenderPostPage(templates *template.Template, version int64, data template.HTML) ([]byte, error) {
var pageData bytes.Buffer
context := map[string]any{
"version": version,
"renderingTimestamp": time.Now().Unix(),
"data": p.Data,
"modTimestamp": p.Timestamp,
"data": data,
}
if err := templates.ExecuteTemplate(&pageData, PostPageTmplName, context); err != nil {

View File

@ -1,28 +1,23 @@
{{ define "footer" }}
<footer>
<div class="rotating-pic-container">
<img src="/assets/pic/heh.png?v={{ .version }}" class="rotating-pic">
<div>
<img src="/assets/pic/footer.webp?v={{ .version }}" width="100%" height="100%">
</div>
<div>
<p>
and also you can subscribe to my <a href="https://t.me/lolistack" target="_blank">telegram channel </a> with pictures!
</p>
</div>
<div>
<p>
<strong>some system information</strong>:
</p>
<p>
> unix timestamp of page rendering: <code>{{ .renderingTimestamp }}</code><br/>
> <code>curl -X LIMINAL https://hikan.ru</code> - what do you know about liminal spaces<br/>
> <code>curl -X COUNT https://hikan.ru</code> - 24-hour server request count<br/>
</p>
<p>
<details>
<summary class="pulse-text">click here if you want more system details</summary>
<p>
> to check the latest changes to the site you can use it (someday i hope to add a cool log page):
<pre>curl -s "https://git.hikan.ru/api/v1/repos/serr/hikan.ru/commits?limit=10" | jq -r '.[] | .commit.author.date + " | " + (.commit.message | rtrimstr("\n"))'</pre>
or look in the repository <a href="https://git.hikan.ru/serr/hikan.ru" target="_blank">git.hikan.ru/serr/hikan.ru</a>
</p>
</details>
</p>
<ul>
<li>unix timestamp of page rendering: <code>{{ .renderingTimestamp }}</code></li>
<li><code>curl -X LIMINAL https://hikan.ru</code> - what do you know about liminal spaces?</li>
<li>this site code repository - <a href="https://git.hikan.ru/serr/hikan.ru" target="_blank">git.hikan.ru/serr/hikan.ru</a></li>
<li><code>curl -X COUNT https://hikan.ru</code> - 24-hour server request count</li>
</ul>
</div>
<div>
<p>

View File

@ -3,7 +3,7 @@
<title>hikan.ru</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="/assets/pic/favicon.ico?v={{ .version }}" type="image/x-icon">
<link rel="shortcut icon" href="/assets/pic/favicon.png?v={{ .version }}" type="image/x-icon">
<link rel="stylesheet" href="/assets/css/styles.css?v={{ .version }}" type="text/css">
</head>
{{ end }}

View File

@ -1,13 +1,7 @@
{{ define "header" }}
<header>
<div class="rotating-pic-container">
<img class="rotating-pic" src="/assets/pic/miffy.png?v={{ .version }}"
style="width: 100%; height: 100%;">
</div>
<div class="pulse-container">
<p class="pulse-text">
<code>🎵&nbsp;last played track: </code><code id="lastfm">waiting for a response from server... (JavaScript is required)</code><code>&nbsp;🎵</code>
</p>
<div>
<img src="/assets/pic/header.webp?v={{ .version }}" width="100%" height="100%">
</div>
<div>
<h1>
@ -18,31 +12,10 @@
</p>
</div>
<div>
<p>
see <a href="/">main page</a> or go to <a href="/posts/">posts section</a>
</p>
<ul>
<li><a href="/">main page</a></li>
<li><a href="/posts/">posts section</a></li>
</ul>
</div>
<div>
<p>
and also you can subscribe to my <a href="https://t.me/lolistack" target="_blank">telegram channel</a> with pictures!
</p>
</div>
<script>
function updateLastfm() {
fetch('/api/lastfm')
.then(response => response.text())
.then(text => {
document.getElementById('lastfm').textContent = text;
})
.catch(error => {
console.error('Error fetching lastfm:', error);
});
}
updateLastfm();
setInterval(updateLastfm, 30000);
</script>
</header>
{{ end }}

View File

@ -4,11 +4,6 @@
<body>
{{ template "header" . }}
<main>
<div>
<p>
<code>welcome to my little site</code>
</p>
</div>
<div>
<h1>
$whoami
@ -28,10 +23,10 @@
what do i do?
</h1>
<p>
programming is my lifestyle
programming is my everything - my job, my hobby, my lifestyle
</p>
<p>
i love growing in all areas of programming - i am literally interested in everything: web development, low-level programming, formal grammars and a lot more!
i love growing in all areas of programming - i am literally interested in everything: cybersecurity (chaotically breaking things, analyzing code, writing automated analyzers, and moving bytes back and forth), concurrency/multithreading, web development, low-level programming, cryptography and a lot more!
</p>
<p>
i like the idea of <a href="https://en.wikipedia.org/wiki/Symbolic_execution" target="_blank">symbolic</a>/concolic execution and virtual code execution in general
@ -41,34 +36,34 @@
<h1>
things i love
</h1>
<p>
<strong>> coffee</strong>. i REALLY love coffee. almost any. and a lot of<br/>
<strong>> movies and TV series</strong> (especially TV series). i watch something almost every day<br/>
<strong>> true crime</strong>. i'm obsessed with serial killer cases, mysterious disappearances, unsolved murders - all that dark stuff<br/>
<strong>> russian underground rap</strong> like Slava KPSS, Zamay, MB Packet, Ovsyankin etc.<br/>
<strong>> simple and extensible code</strong>. i think if your code is overly complex, it means you are doing something wrong. most things are simpler than they seem
</p>
<ul>
<li><strong>coffee</strong>. i REALLY love coffee. almost any. and a lot of</li>
<li><strong>movies and TV series</strong> (especially TV series). i watch something almost every day</li>
<li><strong>true crime</strong>. i'm obsessed with serial killer cases, mysterious disappearances, unsolved murders - all that dark stuff</li>
<li><strong>russian underground rap</strong> like Slava KPSS, Zamay, MB Packet, Ovsyankin etc.</li>
<li><strong>simple and extensible code</strong>. i think if your code is overly complex, it means you are doing something wrong. most things are simpler than they seem</li>
</ul>
</div>
<div>
<h1>
projects
</h1>
<p>
> <a href="https://git.hikan.ru/serr/candycache" target="_blank">git.hikan.ru/serr/candycache</a> - simple and efficient Go cache<br/>
> <a href="https://git.hikan.ru/serr/eye-hot-reloader" target="_blank">git.hikan.ru/serr/eye-hot-reloader</a> - lightweight directories monitor with automatic rebuild and restart functionality for any project type<br/>
> <del>telegram bot with schedule for SPBPU - <a href="https://t.me/polysched_bot" target="_blank">polysched_bot</a></del> (transferred to a more proactive owner)<br/>
> <del>telegram bot with schedule for SPMI - <a href="https://t.me/gornischedule_bot" target="_blank">gornischedule_bot</a></del> (closed)<br/>
</p>
<ul>
<li><a href="https://git.hikan.ru/serr" target="_blank">git.hikan.ru/serr</a> - check my repos</li>
<li><del>telegram bot with schedule for SPBPU - <a href="https://t.me/polysched_bot" target="_blank">polysched_bot</a></del> (transferred to a more proactive owner)</li>
<li><del>telegram bot with schedule for SPMI - <a href="https://t.me/gornischedule_bot" target="_blank">gornischedule_bot</a></del> (closed)</li>
</ul>
</div>
<div>
<h1>
nice links
</h1>
<p>
> huge collection of Xakep issues - <a href="https://図書館.きく.コム/" target="_blank">図書館.きく.コム</a><br/>
> i like to surf here - <a href="https://neocities.org/browse" target="_blank">neocities.org/browse</a><br/>
> very atmospheric forum about black metal - <a href="https://www.lycanthropia.net/" target="_blank">lycanthropia</a><br/>
</p>
<ul>
<li><a href="https://mo.rijndael.cc/" target="_blank">Mo</a>, thx for design idea!</li>
<li>huge collection of Xakep issues - <a href="https://図書館.きく.コム/" target="_blank">図書館.きく.コム</a></li>
<li>i love this website highlighting the Small Web - <a href="https://smallweb.cc/" target="_blank">smallweb</a></li>
<li>very atmospheric forum about black metal - <a href="https://www.lycanthropia.net/" target="_blank">lycanthropia</a></li>
</ul>
</div>
</main>
{{ template "footer" . }}

View File

@ -6,9 +6,6 @@
<main>
<div>
{{ .data }}
<p>
<code>mod time: {{ .modTimestamp }}</code>
</p>
</div>
</main>
{{ template "footer" . }}

View File

@ -7,66 +7,6 @@
Но в реальности все не так просто и при попытке что то положить по вычисленному адресу программа упадет с *segmentation fault (SIGSEGV)*. Чтобы этого избежать, предварительно надо выдать права на запись в страничку памяти где находится целевая строка. Сделать это можно через системные вызовы.
```
package main
import (
"fmt"
"syscall"
"unsafe"
)
func main() {
s := "hello"
fmt.Println("Original:", s)
// Строки в Go - структуры с двумя полями: указатель на данные и длина
// тут строка интерпретируется как структура
strHeader := (*struct {
data unsafe.Pointer
len int
})(unsafe.Pointer(&s))
// Вычисляем начало страницы памяти
pageSize := syscall.Getpagesize()
pageStart := uintptr(strHeader.data) - (uintptr(strHeader.data) % uintptr(pageSize))
fmt.Printf(
"\nАдрес строки = %d\nразмер страницы памяти = %d\nадрес строки относительно начала страницы = %d\nадрес страницы = %d\n\n",
uintptr(strHeader.data),
uintptr(pageSize),
(uintptr(strHeader.data) % uintptr(pageSize)),
pageStart)
// Имея адрес страницы и ее размер даю права на запись на данной странице памяти
ret, _, err := syscall.Syscall(
syscall.SYS_MPROTECT,
pageStart,
uintptr(pageSize),
uintptr(syscall.PROT_READ|syscall.PROT_WRITE),
)
if ret != 0 {
panic("mprotect failed: " + err.Error())
}
// Теперь можно менять строку
// поменяю, например, пятый байт
fifthBytePtr := (*byte)(unsafe.Pointer(uintptr(strHeader.data) + 4))
*fifthBytePtr = 0
// Восстанавливаю дефолтные права
ret, _, err = syscall.Syscall(
syscall.SYS_MPROTECT,
pageStart,
uintptr(pageSize),
uintptr(syscall.PROT_READ),
)
if ret != 0 {
panic("mprotect restore failed: " + err.Error())
}
fmt.Println("Modified:", s) // Должно быть "hell"
}
```
Код с пояснениями можно скачать [тут](https://git.hikan.ru/serr/unsafe-change-string-go)
Тестил на **go version go1.22.2 linux/amd64**

View File

@ -1,27 +0,0 @@
package tools
import "time"
// Ticker запускает функцию fn с указанным интервалом в отдельной горутине
// Возвращает канал для остановки тикера
func Ticker(fn func(), interval time.Duration) chan struct{} {
if interval <= 0 {
interval = time.Second
}
ticker := time.NewTicker(interval)
stopChan := make(chan struct{})
fn()
go func() {
for {
select {
case <-ticker.C:
fn()
case <-stopChan:
ticker.Stop()
return
}
}
}()
return stopChan
}