# SSH App programmieren



## maksimilian (5. Mrz 2021)

Hallo Ihr,
ich möchte für eine Pi-Anwendung (Türsteuerung) eine Fernbedienung mittels einer Handy-App (Samsung SM-A310F) implementieren. Mir ist bekannt, dass im PlayStore haufenweise SSH-Button Apps angeboten werden. Für Tests ist bei mir auch so eine App in Verwendung.  Ich möchte aber schon allein wegen fachlicher Neugier die App selber implementieren und dabei auch eigene Vorstellungen berücksichtigen. Die Start-Funktionalität bestünde in einer Oberfläche mit einigen Buttons, hinter welchen sich der Aufruf eines Python-Skripts im Pi-System verbergen würde, welches über eine Pipe mit der Steuerung kommuniziert.

Da ich sowohl mit Kotlin/Java/notwendige Bibliotheken und Android Studio noch nicht so vertraut bin, bräuchte ich Start-Hilfe. Eigentlich wollte ich die Implementierung mit Kotlin vornehmen, habe aber bei meiner Recherche für SSH noch keinen Ansatz gefunden. Interessant wäre für mich in dem Zusammenhang, ob Java  in Kotlin aufgerufen werden kann.

Bin dann auf diesen Link gestoßen, der sich aber auf einen älteren Thread bezieht.

maksimilian


----------



## Robert Zenz (5. Mrz 2021)

Die bessere Variante waere es, wenn du am Pi einen Server hast welcher eine API zur Verfuegung stellt, und deine App kommuniziert dann nur mit dem Server. Hat einige Vorteile, aber einer der wichtigsten ist dass deine App dann keinen SSH Zugang auf die Schuessel braucht, und die Kommandoes welche ueber die Apps kommen koennen sind gut definiert.


----------



## maksimilian (5. Mrz 2021)

Danke für den Hinweis, Robert. Dass der Zugriff auf den Pi mal anders organisiert werden sollte, ist mir schon klar. Aber ich möchte schrittweise vorgehen, ert mal die SSH-App.


----------



## kneitzel (5. Mrz 2021)

Bezüglich Zugriff auf Java empfehle ich einfach einmal:








						Calling Java from Kotlin | Kotlin
					






					kotlinlang.org
				




Des Weiteren hast Du bei der Android App Entwicklung mit Kotlin doch auch in der Regel Gradle. Du kannst also ganz normal die Dependencies zu den Libraries, die du brauchst, eintragen.

Daher kannst Du da auch einfach jsch für nutzen:





						Maven Repository: com.jcraft » jsch
					






					mvnrepository.com
				




Evtl. ist es einfacher, erst einmal auf dem PC damit zu spielen um dann den Wechsel hin zu Android zu machen, wenn Du die Library etwas kennen gelernt hast. Da ist es halt einfacher, da Du alles direkt in der IDE ausführen kannst ohne Android VM oder smartphone.


----------



## maksimilian (6. Mrz 2021)

Danke für Hinweise und Links, kneitzel ! Ich versuche  erst mal  als Test mit einem Button über jsch SSH anzuwenden.


----------



## maksimilian (6. Mrz 2021)

Ich verwende in MainActivity.kt 

import com.jcraft.jsch.ChannelExec

und erhalte die Fehlermeldung Unresolved reference: jcraft

Wo müssen die Klassen-Definitionen von jsch in der Datei/Projekt-Hierarchie stehen ?


----------



## kneitzel (6. Mrz 2021)

Hast Du denn jsch als Abhängigkeit in der build.gradle Datei eingetragen?

Edit: Link zur Beschreibung 








						Add build dependencies  |  Android Developers
					

Learn how to add build dependencies using the Gradle build system in Android Studio.




					developer.android.com


----------



## maksimilian (6. Mrz 2021)

Danke für den Tipp, ich kriege aber die korrekte Syntax nicht hin. Es gibt die Datei jsch-0.1.55.jar unter $HOME/android-studio/lib/jsch-0.1.55.jar, welche man sich auch von mvnrepository.com herunterladen kann. Ich ergänze die Datei build.gradle wie folg (ins Blaue):


```
dependencies {
        classpath "com.android.tools.build:gradle:4.1.2"
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        classpath "com.jcraft.jsch:jsch-0.1.55"
}
```

Build Output ist:
! Build: failed at .. with 1 error:
     ! Could not resolve com.jcraft.jsch:jsch-0.1.55:
     ! Could not find com.jcraft.jsch:jsch-0.1.55:

Was muss hinter dem Doppelpunkt stehen ? Oder muss ich ein repository definieren (und dann wie)?


----------



## kneitzel (6. Mrz 2021)

Der Ablauf ist nicht, dass Du die jar Datei von Hand herunter lädst - Das macht gradle automatisch.

Den richtigen Eintrag zeigt Dir das mvnrepository auch (daher der Link):





						Maven Repository: com.jcraft » jsch
					






					mvnrepository.com
				



- Klicke dort erst auf die Version, die Du nutzen willst (also am Besten die aktuelle: 0.1.55
- Dann hast Du auf der Seite unter der kleinen Tabelle eine reihe Tabs: Maven, Gradle, ... Dort mal auf Gradle gehen

```
// https://mvnrepository.com/artifact/com.jcraft/jsch
implementation group: 'com.jcraft', name: 'jsch', version: '0.1.55'
```

Damit hast Du dann schon den Eintrag, der notwendig ist. Genau das kannst Du 1:1 in die dependencies kopieren.

Aber Du bist vermutlich auch im falschen build.gradle File. Du dürftest derzeit in dem Hauptfile sein. Durch classpath werden jetzt nur Projekt Abhängigkeiten für Plugins und so definiert. Das sind jetzt keine Compile-Zeit Abhängigkeiten.
Du hast ein Unterverzeichnis (Vermutlich "app") mit einem weiteren build.gradle und in dem musst Du die Abhängigkeit wie oben abgeben.

Daher hattet Du vermutlich auch einen Kommentar bei diesen Kommentaren wie den hier:


```
// NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
```


----------



## maksimilian (7. Mrz 2021)

Der Tipp bringt mich weiter. Jetzt habe ich das Problem, dass der Verbindungsaufbau scheitert. Ich verwende folgendes Testprogramm


```
package com.example.ereignisse

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.example.ereignisse.databinding.ActivityMainBinding

import com.jcraft.jsch.ChannelExec
import com.jcraft.jsch.JSch
import com.jcraft.jsch.Session
import java.io.ByteArrayOutputStream
import java.util.*

class MainActivity : AppCompatActivity() {
    private lateinit var B: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        B = ActivityMainBinding.inflate(layoutInflater)
        setContentView(B.root)

        B.SSH.setOnClickListener {
            B.Ausgabe.text = "SSH betaetigt"
            executeRemoteCommand(
                    "pi",
                    "pass",
                    "192.168.178.32",
                    22,
                    "ps>x"
            )
        }
    }


    fun executeRemoteCommand(
            username: String?,
            password: String?,
            hostname: String?,
            port: Int,
            message: String?
    ): String? {
        val jsch = JSch()
        val session: Session = jsch.getSession(username, hostname, port)
        session.setPassword(password)

        // Avoid asking for key confirmation
        val prop = Properties()
        prop["StrictHostKeyChecking"] = "no"
        session.setConfig(prop)
        try {
            session.connect()
        }
        catch (ex:Exception) {
                B.Ausgabe.text = ex.toString()
        }

        // SSH Channel
        val channelssh: ChannelExec = session.openChannel("exec") as ChannelExec
        val baos = ByteArrayOutputStream()

        try {
            channelssh.setOutputStream(baos)
            // Execute command
            channelssh.setCommand(message)
            channelssh.connect()
            channelssh.disconnect()
            }
        catch (ex:Exception) {
            B.Ausgabe.text = ex.toString()
        }
        finally {
            return baos.toString()
        }

    }
```

Der Button _SSH _löst einen SSH-Aufruf aus. Trotz Abschalten der Key-Abfrage scheitert der connect in Zeile 50 mit der Exception-Meldung:

```
com.jcraft.jsch.JSchException: java.net.SocketException: Permission denied
```

Bei der SSH-Button App, welche ich bisher zum Testen vewende, funktioniert die Verbindung mit den gleichen Verbindungsdaten.


----------



## maksimilian (8. Mrz 2021)

Gibt es eine Möglichkeit, mehr Informationen über die zurückgewiesene connection zu erhalten ? Genauere Exception-Behandlung ? Log-Datei ?


----------



## kneitzel (8. Mrz 2021)

Du machst es ja in einer Android Applikation. Hast Du die notwendigen Rechte im Manifest eingetragen?


----------



## mihe7 (8. Mrz 2021)

maksimilian hat gesagt.:


> Log-Datei ?


Reicht das Logcat in Android Studio nicht?


----------



## maksimilian (8. Mrz 2021)

# 12 Jetzt klappt's, kneitzel ! Du hast mir sehr geholfen.


----------



## maksimilian (8. Mrz 2021)

#13 DieException vom connect() meldete "Permission denied", was ja alles Mögliche bedeuten kann. Aber jetzt ist (fast) alles gut.


----------



## mihe7 (8. Mrz 2021)

maksimilian hat gesagt.:


> #13 DieException vom connect() meldete "Permission denied", was ja alles Mögliche bedeuten kann


Ja, ich meinte nur, ob man im Logcat nicht mehr sieht. Bin schon eine Zeit lang nicht mehr an Android gesessen, meinte mich aber zu erinnern, dass da durchaus Hinweise auf das Manifest zu finden waren. Abgesehen davon: permission denied ist schon ein guter Indikator, dass man sich mal die Rechte ansieht


----------



## maksimilian (8. Mrz 2021)

#16 Hatte in #13 die falsche Antwort gegeben. Sorry, ich lerne noch. Ich habe jetzt mal den Fehler rekonstruiert und im Logcat verfolgt. Da ist mir aber ein Hinweis auf Manifest nicht gelungen. Das kann aber auch daran liegen, dass ich die Hinweise auf bestimmte Zeilen in bestimmten Modulen (z.B. ThreadPoolExecutor.java) nicht verfolgen konnte, da ich nicht weiß, wie man in den Source dieser Module gelangt. Aber letzlich ein wichtiger Hinweis, der mir in Zukunft nützlich sein kann ! 
Auch bei evtl. zukünftigen Fehlermeldungen "Permission denied" bin ich natürlich jetzt "hellsichtiger"


----------



## maksimilian (12. Mrz 2021)

Lässt sich SHH beschleunigen ? Der zeitliche Verzug des Aufrufs einer Anwendung am Pi gegenüber dem am Pi selbst (Taster) ist auffällig.


----------



## kneitzel (12. Mrz 2021)

Also generell ist klar, dass es hier Verzögerungen geben muss. Aber nur um sicher zu gehen: Von was für Verzögerungen reden wir hier? Geht es dir um Sekunden? Oder um Millisekunden?

Generell gilt hier wie bei allen Dingen: Wenn das Laufzeitverhalten nicht ausreichend ist, dann muss zuerst analysiert werden, was für Probleme denn genau herrschen:
Wo braucht die Applikation denn wie lange?

Dann kann man überlegen, was für Schritte denn etwas bringen oder eben nichts bringen. Wenn Du a, b und c machst und das zusammen dir zu lange dauert ("Da wartet der Benutzer 10 Sekunden, dauert viel zu lange!") und dann fängst Du an, b zu optimieren. Super - ist super Optimiert - b braucht nur noch 1% der vorherigen Zeit.... Aber leider war es so, dass b vorher nur 0,1s gebraucht hat. Super optimiert ist es nur noch 0,001s - aber das Problem besteht immer noch, denn der Benutzer wartet immer noch 10s - die 0,099s Optimierung merkt er nicht!)

Hoffe es wird deutlich, was bei Optimierungen das Problem ist.


----------



## maksimilian (12. Mrz 2021)

OK, ich muss versuchen, mittels Zeitstempel, die Abläufe zeitlich zu analysieren.


----------



## mihe7 (12. Mrz 2021)

Das kann verschiedene Ursachen haben, z. B. wenn UseDNS in der sshd_config auf Yes steht.


----------



## maksimilian (12. Mrz 2021)

Das ist nicht der Fall. Danke für den Hinweis.


----------



## maksimilian (12. Mrz 2021)

Ich habe jetzt noch eine grundsätzliche Frage. Ich lasse die im Code von #10 beschriebene Prozedur ExecuteRemoteCommand() im Hintergrund laufen (mit doAsync). Wie kann der Main-Thread einen Rückgabewert erhalten. mit return geht das ja dann nicht.


----------

