diff --git a/install_script.sh b/install_script.sh new file mode 100644 index 0000000..9f72b39 --- /dev/null +++ b/install_script.sh @@ -0,0 +1,81 @@ +#!/bin/bash +# Description: Скрипт для корректной загрузки и установки программы RPIVirusScanner +# Author: Шафеев "Thiflict" Марат +# Date: 2026-04-14 + +# Так как скрипт не тестировался должным образом, необходимо запросить у пользователя о его намерениях продолжать выполнение скрипта. +read -p "ВНИМАНИЕ!!! Данный скрипт установки не тестировался должным образом! Это значит, что скрипт может поломать некоторые конфиги ClamAV и Samba КАК МИНИМУМ! Для продолжения введите: 'i understand all the risks and want to continue'. Это будет означать, что вы понимаете риски и хотите продолжить выполнение программы! " answer +answer=${answer,,} + +if [[ "$answer" != "i understand all the risks and want to continue" ]]; then + echo "Неправильно набрана фраза для продолжения. Отменяю..." + exit 0 +fi +echo "Выполняю..." + +# Обновление списков пакетов и зарузка зависимостей +apt update; +apt install openjdk-21-jdk gpiod libgpiod-dev clamav clamav-daemon ntfs-3g usbmount samba -y; # Загрузка нужных пактов + +# Настройка либы для взаимодействия с GPIO +echo "Подготовка библиотек взаимодействия с GPIO..." +sudo usermod -a -G gpio,spi,i2c $USER; +newgrp gpio; + +# Настройка ClamAV +echo "Настройка ClamAV" +{ + echo "PrivateMirror https://clamav-mirror.ru"; + echo "PrivateMirror https://mirror.truenetwork.ru/clamav/"; + echo "PrivateMirror http://mirror.truenetwork.ru/clamav/"; + echo "ScriptedUpdates no"; +} >> /etc/clamav/freshclam.conf; +rm /var/lib/clamav/freshclam.dat; +freshclam -vvv; + +CONFIG_FILE="/etc/clamav/clamd.conf"; # Путь к файлу конфига +NEW_VALUE_MT="10"; # Новое значение MaxThreads +NEW_VALUE_MF="10M"; # Новое значение MaxFileSize +NEW_VALUE_MS="30M"; # Новое значение MaxScanSize + +cp "$CONFIG_FILE" "$CONFIG_FILE.bak"; # Создание бекапа конфига +sed -i -E " + s/^(MaxThreads[[:space:]]+).*/\1$NEW_VALUE_MT/ + s/^(MaxFileSize[[:space:]]+).*/\1$NEW_VALUE_MF/ + s/^(MaxScanSize[[:space:]]+).*/\1$NEW_VALUE_MS/ +" "$CONFIG_FILE"; # Изменение конфига + +# Загрузка временных файлов +echo "Загрузка необходиых временных файлов..." +rm -rf /tmp/rpivs; +mkdir /tmp/rpivs; +wget https://static1.thiflict.ru/raspberrypi_virus_scanner/v2.0/smb_conf -o /tmp/rpivs/smb.conf +wget https://static1.thiflict.ru/raspberrypi_virus_scanner/v2.0/rpivirusscanner.jar -o /tmp/rpivs/rpivirusscanner.jar; +wget https://static1.thiflict.ru/raspberrypi_virus_scanner/v2.0/rpi_virus_scanner_service -o /tmp/rpivs/rpi_virus_scanner.service; + +# Настройка Samba +echo "Выполняется настройка Samba" +systemctl stop smbd +systemctl disable smbd +cp /tmp/rpivs/smb.conf /etc/samba/smb.conf +testparm # Для дебага + +# Установка программы +echo "Установка программы..." +rm -rf /opt/thiflict-dev/rpi-virus-scanner/ +mkdir -p /opt/thiflict-dev/rpi-virus-scanner/ +mv /tmp/rpivs/rpivirusscanner.jar /opt/thiflict-dev/rpi-virus-scanner/; + +# Создание демона +echo "Создание демона RPIVirusScanner..." +mv /tmp/rpivs/rpi_virus_scanner.service /etc/systemd/system/; +systemctl daemon-reload; +systemctl enable rpi_virus_scanner.service; + +# Авторство: +echo "RaspberryPi успешно установлен! Рекомендуется перезагрузить устройство."; +echo " "; +echo "==== Об авторе: ===="; +echo "Репозиторий проекта: https://git.ds1.thiflict.ru/Shafeev-CourseWork/Practice-Part"; +echo "Телеграм-канал: https://t.me/Thiflict_tg"; +echo "YouTube канал: https://youtube.com/@Thiflict"; \ No newline at end of file diff --git a/readme.md b/readme.md index 8a231f8..7bffbb9 100644 --- a/readme.md +++ b/readme.md @@ -43,4 +43,6 @@ sudo chmod +x install_script.sh Затем, выполнить его от имени суперпользователя: ```bash sudo ./install_script.sh -``` \ No newline at end of file +``` + +# Development: stable коммит сейчас является [a655f026](https://git.ds1.thiflict.ru/Shafeev-CourseWork/Practice-Part/commit/a655f026b4e583bf736355d6b0f161f47c620235) \ No newline at end of file diff --git a/src/main/kotlin/ButtonListener.kt b/src/main/kotlin/ButtonListener.kt index 4aa9077..fd38e1e 100644 --- a/src/main/kotlin/ButtonListener.kt +++ b/src/main/kotlin/ButtonListener.kt @@ -7,11 +7,11 @@ import java.io.BufferedReader import java.io.InputStreamReader import java.util.concurrent.atomic.AtomicBoolean -// Событие клика по кнопке class ButtonClickEvent +class ButtonLongPressEvent -// Слушатель событий клика typealias ButtonClickListener = (ButtonClickEvent) -> Unit +typealias ButtonLongPressListener = (ButtonLongPressEvent) -> Unit class ButtonListener( private var process: Process? = null, @@ -19,63 +19,128 @@ class ButtonListener( private var isListening: AtomicBoolean = AtomicBoolean(false), private var listenerJob: Job? = null ) { - private val clickListeners = mutableListOf() + private val longPressListeners = mutableListOf() + + var shortPressThresholdMs: Long = 100 + var longPressThresholdMs: Long = 1000 + + private var pressStartTime: Long = 0 fun addClickListener(listener: ButtonClickListener) { clickListeners.add(listener) } + fun removeClickListener(listener: ButtonClickListener) { + clickListeners.remove(listener) + } + + fun addLongPressListener(listener: ButtonLongPressListener) { + longPressListeners.add(listener) + } + + fun removeLongPressListener(listener: ButtonLongPressListener) { + longPressListeners.remove(listener) + } + private fun notifyClick() { + println("=== [CLICK] Короткое нажатие ===") val event = ButtonClickEvent() clickListeners.forEach { it.invoke(event) } } + private fun notifyLongPress() { + println("=== [LONG] Длинное нажатие ===") + val event = ButtonLongPressEvent() + longPressListeners.forEach { it.invoke(event) } + } + fun startListening(onStateChange: ((Boolean) -> Unit)? = null): Boolean { if (isListening.get()) { return currentButtonState.get() } + startListeningInternal(onStateChange) + return currentButtonState.get() + } + + private fun startListeningInternal(onStateChange: ((Boolean) -> Unit)? = null) { try { val cmd = listOf("gpiomon", "--chip=0", "$BUTTON") val processBuilder = ProcessBuilder(cmd) .redirectErrorStream(true) - // Сохраняем процесс в поле process = processBuilder.start() isListening.set(true) listenerJob = CoroutineScope(Dispatchers.IO).launch { - // Используем process, который теперь не null val reader = BufferedReader(InputStreamReader(process!!.inputStream)) var line: String? - while (reader.readLine().also { line = it } != null) { - println(if (isDebug) "$line" else "") - when { - line?.contains("rising") == true -> { - currentButtonState.set(true) - onStateChange?.invoke(true) - println(if (isDebug) "Кнопка нажата" else "") - // Генерируем событие клика при нажатии - notifyClick() - } - line?.contains("falling") == true -> { - currentButtonState.set(false) - onStateChange?.invoke(false) - println(if (isDebug) "Кнопка разжата" else "") - // Убрана проверка на разжатие для генерации события + try { + while (reader.readLine().also { line = it } != null) { + println(if (isDebug) "$line" else "") + when { + line?.contains("rising") == true -> { + currentButtonState.set(true) + onStateChange?.invoke(true) + pressStartTime = System.currentTimeMillis() + println(if (isDebug) "Кнопка нажата, время: $pressStartTime" else "") + } + line?.contains("falling") == true -> { + val pressDuration = System.currentTimeMillis() - pressStartTime + currentButtonState.set(false) + onStateChange?.invoke(false) + println(if (isDebug) "Кнопка отпущена, длительность: ${pressDuration}ms" else "") + + // Определяем тип нажатия по длительности + when { + pressDuration >= longPressThresholdMs -> { + println(if (isDebug) "Длинное нажатие (${pressDuration}ms)" else "") + notifyLongPress() + } + pressDuration >= shortPressThresholdMs -> { + println(if (isDebug) "Короткое нажатие (${pressDuration}ms)" else "") + notifyClick() + } + else -> { + println(if (isDebug) "Слишком короткое нажатие (${pressDuration}ms) - игнорируем" else "") + } + } + } } } + } catch (e: Exception) { + println("Ошибка чтения процесса: ${e.message}") } + val exitCode = process!!.waitFor() - println(if (isDebug) "Процесс завершён ($exitCode)" else "") + println("Процесс завершён ($exitCode)") isListening.set(false) + + // Автоматически перезапускаем прослушивание, если оно должно быть активно + if (isListening.get()) { + println("Перезапуск прослушивания...") + delay(100) + startListeningInternal(onStateChange) + } } } catch (e: Exception) { e.printStackTrace() isListening.set(false) } - return currentButtonState.get() + } + + fun stopListening() { + isListening.set(false) + listenerJob?.cancel() + process?.destroy() + process = null + } + + fun restartListening(onStateChange: ((Boolean) -> Unit)? = null) { + stopListening() + Thread.sleep(200) + startListening(onStateChange) } } \ No newline at end of file diff --git a/src/main/kotlin/Main.kt b/src/main/kotlin/Main.kt index ba880e5..de75146 100644 --- a/src/main/kotlin/Main.kt +++ b/src/main/kotlin/Main.kt @@ -8,6 +8,7 @@ import java.io.InputStreamReader import kotlin.system.exitProcess import java.util.* import java.util.concurrent.* +import kotlin.time.Duration.Companion.milliseconds // Конфигурация GPIO (BCM номера) object PinConfig { @@ -27,7 +28,7 @@ object DebugMode { // Состояния системы enum class SystemState { - IDLE, SCANNING, CLEAN, INFECTED, ERROR, INIT + IDLE, SCANNING, CLEAN, INFECTED, ERROR, INIT, SAMBA } class USBVirusScanner { @@ -120,45 +121,56 @@ class USBVirusScanner { greenLed.setValue(1) yellowLed.setValue(1) redLed.setValue(1) - delay(200) + delay(200.milliseconds) greenLed.setValue(0) yellowLed.setValue(0) redLed.setValue(0) - delay(200) + delay(200.milliseconds) } + SystemState.IDLE -> { // Зелёный медленно мигает blinkLed(greenLed, 1000) yellowLed.setValue(0) redLed.setValue(0) } + SystemState.SCANNING -> { // Жёлтый быстро мигает greenLed.setValue(0) blinkLed(yellowLed, 150) redLed.setValue(0) } + SystemState.CLEAN -> { // Зелёный горит greenLed.setValue(1) yellowLed.setValue(0) redLed.setValue(0) - delay(100) + delay(100.milliseconds) } + SystemState.INFECTED -> { // Красный горит greenLed.setValue(0) yellowLed.setValue(0) redLed.setValue(1) - delay(100) + delay(100.milliseconds) } + SystemState.ERROR -> { // Красный мигает greenLed.setValue(0) yellowLed.setValue(0) blinkLed(redLed, 500) } + + SystemState.SAMBA -> { + blinkLed(greenLed, 200) + blinkLed(yellowLed, 200) + redLed.setValue(0) + } } } } @@ -166,9 +178,9 @@ class USBVirusScanner { private suspend fun blinkLed(led: GPIOController, intervalMs: Long) { led.setValue(1) - delay(intervalMs / 2) + delay((intervalMs / 2).milliseconds) led.setValue(0) - delay(intervalMs / 2) + delay((intervalMs / 2).milliseconds) } fun startScan() { @@ -188,7 +200,7 @@ class USBVirusScanner { if (usbDevices.isEmpty()) { println(if (isDebug) "Устройства не найдены" else "") currentState = SystemState.ERROR - delay(5000) + delay(5000.milliseconds) currentState = SystemState.IDLE return@launch } @@ -216,6 +228,7 @@ class USBVirusScanner { if (result.infectedFiles.size > 10) { println(if (isDebug) " ... и ещё ${result.infectedFiles.size - 10} файлов" else "") } + unmount() } else if (result.error != null) { println(if (isDebug) "❌ Ошибка сканирования: ${result.error}" else "") } else { @@ -241,14 +254,31 @@ class USBVirusScanner { println(if (isDebug) message else "") // Держим состояние 5 секунд - delay(5000) + delay(10000.milliseconds) + + if (!hasVirus) { + currentState = SystemState.SAMBA + sambaStartSharing() + + val buttonListener = ButtonListener() + var isEventHappened = false + + buttonListener.addLongPressListener { event -> + isEventHappened = true + } + buttonListener.startListening() + while (!isEventHappened) { + delay(500.milliseconds) + } + } + currentState = SystemState.IDLE } catch (e: Exception) { println(if (isDebug) "❌ Критическая ошибка: ${e.message}" else "") e.printStackTrace() currentState = SystemState.ERROR - delay(5000) + delay(10000.milliseconds) currentState = SystemState.IDLE } @@ -295,7 +325,8 @@ class USBVirusScanner { if ((mountPoint.startsWith("/media")) && File(mountPoint).exists() && - devices.none { it.mountPoint == mountPoint }) { + devices.none { it.mountPoint == mountPoint } + ) { devices.add(USBDevice("auto", mountPoint)) } @@ -337,7 +368,7 @@ class USBVirusScanner { infectedFiles = emptyList(), virusNames = emptyMap(), exitCode = -1, - error = if (isDebug)"Путь не существует или не является директорией: $path" else "" + error = if (isDebug) "Путь не существует или не является директорией: $path" else "" ) } @@ -345,12 +376,10 @@ class USBVirusScanner { // Запускаем ClamAV val processBuilder = ProcessBuilder( - "clamscan", - "-r", + "clamdscan", + "--multiscan", "--stdout", "--infected", - "--max-filesize=1M", - "--max-scansize=10M", path ) @@ -382,7 +411,7 @@ class USBVirusScanner { ) } catch (e: Exception) { - println(if (isDebug) " Ошибка при выполнении clamscan: ${e.message}" else "") + println(if (isDebug) "Ошибка при выполнении clamscan: ${e.message}" else "") return ScanResult( isInfected = false, infectedFiles = emptyList(), @@ -434,6 +463,57 @@ class USBVirusScanner { } } + suspend fun sambaStartSharing() { + try { + currentState = SystemState.SAMBA + + var process: Process? = null + val sambaStartServiceCmd = listOf("systemctl", "start", "smbd") + val processBuilder = ProcessBuilder(sambaStartServiceCmd) + .redirectErrorStream(true) + + process = withContext(Dispatchers.IO) { + processBuilder.start() + } + } catch (e: Exception) { + e.printStackTrace() + currentState = SystemState.ERROR + delay(10000.milliseconds) + } + } + + fun sambaShutdown() { + unmount() + try { + var process: Process? = null + val sambaStopServiceCmd = listOf("systemctl", "stop", "smbd") + val processBuilder = ProcessBuilder(sambaStopServiceCmd) + .redirectErrorStream(true) + process = processBuilder.start() + scanJob?.cancel() + currentState = SystemState.IDLE + } catch (e: Exception) {} + } + + + fun unmount() { + for (i in 0..9) { + val path = if (i == 0) "/media/usb" else "/media/usb$i" + try { + val processBuilder = ProcessBuilder("umount", "-f", path) + .redirectErrorStream(true) + val process = processBuilder.start() + val exitCode = process.waitFor() + if (exitCode == 0) { + println(if (isDebug) "Успешно размонтировано: $path" else "") + } + } catch (e: Exception) { + println(if (isDebug) "Ошибка при размонтировании $path: ${e.message}" else "") + } + } + + } + fun shutdown() { println(if (isDebug) "\nЗавершение работы..." else "") isRunning = false @@ -529,7 +609,6 @@ fun main(args: Array) { println(if (isDebug) "Красный - ${PinConfig.RED_LED}" else "") println(if (isDebug) "Кнопка - ${PinConfig.BUTTON}" else "") - val defaultButtonState = false val scanner = USBVirusScanner() Runtime.getRuntime().addShutdownHook(Thread { @@ -540,12 +619,19 @@ fun main(args: Array) { scanner.initialize() val buttonListener = ButtonListener() + buttonListener.addClickListener { event -> scanner.startScan() } + + buttonListener.addLongPressListener { event -> + scanner.sambaShutdown() + } + buttonListener.startListening() - println(if (isDebug) """ + println( + if (isDebug) """ ═══════════════════════════════════════ USB Virus Scanner готов к работе ═══════════════════════════════════════ @@ -555,7 +641,8 @@ fun main(args: Array) { 📌 Зелёный LED горит - флешка чиста 📌 Красный LED горит - найдены вирусы 📌 Для выхода нажмите Ctrl+C - """.trimIndent() else "") + """.trimIndent() else "" + ) while (true) { Thread.sleep(1000)