Viele VFP-Anwendungen haben in einer Ecke einen PC mit einer offenen exe und einem Timer darin: Er importiert die Bestellungen aus dem Webshop, verschickt die ausstehenden E-Mails, macht jede Nacht den Abschluss. Das funktioniert, solange niemand ihn schließt, sich niemand abmeldet und Windows nicht für ein Update neu startet.

In diesem Tutorial bauen wir pedidos (spanisch für „Bestellungen“), einen Windows-Dienst in Visual FoxPro, der diese Arbeit erledigt, ohne dass jemand davor sitzt. Er importiert die Bestellungen, die ein Webshop in einem Ordner ablegt, zählt alle 30 Sekunden die wartenden, schreibt jede Nacht eine Zusammenfassung und schickt jeden Morgen die offenen ans Lager. Er startet mit dem Rechner, läuft weiter, auch wenn sich niemand anmeldet, und startet von selbst neu, wenn er abstürzt.

Fast den ganzen Code bringt das Projekt schon mit: FoxService legt jeden neuen Dienst mit diesem Import an, und er funktioniert beim ersten Start. Wir gehen ihn durch, fügen einen eigenen Job hinzu und installieren ihn.

Ich habe es mit FoxForge auf Spanisch gebaut, deshalb sind die Screenshots auf Spanisch. FoxForge selbst spricht Ihre Sprache: Die Menünamen und die Schaltflächen gebe ich auf Deutsch an.

Was Sie brauchen

  • Visual FoxPro 9 (oder VFP Advanced 10.2 in 32 Bit).
  • FoxCli installiert. Es bringt FoxForge mit, die Werkstatt, die sich in die VFP-IDE einklinkt, und FoxService. Ohne Lizenz funktioniert alles genauso; den Unterschied sehen wir am Ende.
  • Windows 10 oder 11. Fehlt die 32-Bit-Laufzeit von .NET 10, bietet der Installer von FoxCli an, sie herunterzuladen und zu installieren: Der Dienst braucht sie zum Starten.

Öffnen Sie VFP wie immer, ohne „Als Administrator ausführen“. Beim Kompilieren als Administrator registriert VFP die Komponente in HKLM, und diese Registrierung stört die folgenden Kompilierungen. Den Dienst zu installieren braucht dagegen Administratorrechte, aber die fragt Windows im richtigen Moment ab, mit seinem üblichen Fenster.

Schritt 1. FoxForge laden

Im VFP-Befehlsfenster:

FoxForge in der IDE laden
DO "C:\Program Files (x86)\FoxForge\FoxForge.app"

Passen Sie den Pfad an, wenn Sie FoxForge in einen anderen Ordner installiert haben. In der VFP-Menüleiste erscheint ein neues Menü, FoxForge, und von dort aus geschieht alles Weitere.

Schritt 2. Das Projekt anlegen

FoxForge > Neues Projekt > Windows-Dienst (FoxService). Im Fenster „Neuer Windows-Dienst“ braucht es nur zwei Dinge:

  • Dienstname: pedidos. Das ist auch der Name der .exe und der DLL, und der, den Sie in den Windows-Diensten sehen.
  • Ordner: Ihr Projektordner. Ich habe C:\a1\tutorial-svc genommen.

Lassen Sie das voreingestellte Windows-Konto, „Das System (LocalSystem)“: Es darf auf diesem Rechner alles, wie das Programm mit dem Timer, das es ersetzt. Die beiden anderen sind für den Fall, dass der Dienst einen Netzwerkordner erreichen muss, und Was wähle ich? öffnet das Handbuch, das erklärt, wann welches passt. Was unter Erweiterte Optionen… steht, bringt einen Wert mit, der für den Anfang taugt.

Das FoxForge-Formular „Neuer Windows-Dienst“ (auf Spanisch), mit dem Namen pedidos, dem Ordner C:\a1\tutorial-svc und dem gewählten Konto Das System (LocalSystem)

Klicken Sie auf Erstellen. FoxForge legt das Projekt an, öffnet es im Projekt-Manager und begrüßt Sie mit einem Fenster, das sagt, was ein Dienst ist und wo man anfängt: Erste Schritte öffnet das Handbuch und main.prg öffnen die einzige Datei, die Sie anfassen werden.

Das Fenster „Dienst angelegt“ (auf Spanisch): was ein Dienst ist, was Sie schreiben, was das Beispiel mitbringt und wie man es ausprobiert, mit den Schaltflächen Erste Schritte, main.prg öffnen und Schließen

pedidos\
├── pedidos.pjx          das VFP-Projekt
├── foxforge.json        die Kennung als Dienstprojekt
├── foxservice.json      die Konfiguration des Dienstes
├── src\
│   ├── main.prg         Ihre Jobs: das Einzige, was Sie bearbeiten
│   ├── foxservice.prg   die Basisklasse, die zu FoxService gehört
│   ├── config.fpw       reist in der DLL mit
│   └── orders.frx       der Bericht des Beispiels
└── dist\                hier landet der Dienst beim Kompilieren
    ├── data\            die Daten
    └── inbox\           der Ordner, den das Beispiel überwacht, mit einer CSV mit zwei Bestellungen

Schritt 3. Es laufen sehen

Bevor Sie eine Zeile Code lesen, probieren Sie es aus. Strg+F7 kompiliert (FoxForge > Aktuelles Projekt > Dienst kompilieren) und legt den Dienst in dist\ ab. Ohne FoxCli-Lizenz fügt der Hinweis „Dienst kompiliert“ hinzu, dass er im Evaluierungsmodus läuft.

Strg+F8 (In der Konsole ausführen) startet ihn in einer Konsole, mit seinem Log auf dem Bildschirm, ohne etwas zu installieren:

INFO  job PrepareData: orders.dbf created
INFO  job PrepareData: orders.dbf ready: 0 order(s)
INFO  job ImportOrders: orders_0001.csv: 2 new, 0 repeated
INFO  job CountPending: Pending orders: 2

Die Konsole von Strg+F8 mit dem Log des Dienstes: Er legt orders.dbf an, importiert orders_0001.csv mit 2 neuen Bestellungen und zählt nach 30 Sekunden 2 offene

Der Dienst hat die Tabelle orders.dbf angelegt, die CSV importiert, die in inbox\ wartete, und sie nach inbox\processed\ verschoben. Nach 30 Sekunden zählt er die offenen Bestellungen, und danach alle 30 Sekunden wieder. Strg+C hält ihn an.

Schritt 4. Was main.prg mitbringt

Öffnen Sie src\main.prg. Unter einem langen Kommentar, der die Annotationen erklärt, steht die Klasse; ohne den Rumpf der Methoden ist es das hier:

Die Jobs des Beispiels, ohne ihren Code
DEFINE CLASS PedidosService AS FoxServiceBase OF foxservice.prg OLEPUBLIC

    *!* @startup
    PROCEDURE PrepareData                     && beim Start: die Tabelle, falls es sie nicht gibt

    *!* @every 30s
    PROCEDURE CountPending                    && alle 30 Sekunden: die offenen

    *!* @watch inbox *.csv
    PROCEDURE ImportOrders(tcPath, tcChange)  && jede CSV, die in inbox\ ankommt

    *!* @daily 23:55
    PROCEDURE DailySummary                    && jede Nacht: die Zusammenfassung des Tages

    PROCEDURE RebuildSummary                  && ohne *!*-Zeile: nur, wenn Sie ihn starten

    PROTECTED PROCEDURE OpenOrders            && Hilfsmethode: öffnet die Tabelle und wählt sie aus
    PROTECTED PROCEDURE WriteSummary          && Hilfsmethode: was beide Zusammenfassungen tun

ENDDEFINE

Damit sieht man schon, wie alles funktioniert:

  • Jede öffentliche Methode der Klasse ist ein Job, und die *!*-Zeile darüber sagt, wann er läuft: in festen Abständen (@every), zu einer Uhrzeit (@daily), beim Start (@startup) oder wenn eine Datei in einem Ordner ankommt (@watch). Ohne *!*-Zeile läuft der Job nicht von selbst: Sie starten ihn.
  • Was kein Job ist, wird PROTECTED PROCEDURE. OpenOrders öffnet die Tabelle, legt sie beim ersten Mal mit ihrem Index an und lässt sie ausgewählt; alle Jobs fangen damit an, also ist es egal, welcher zuerst läuft.
  • Die Pfade kommen aus THIS.Path("data"), dem Ordner data aus foxservice.json, und nie aus SET DEFAULT: Das aktuelle Verzeichnis gehört dem ganzen Prozess.
  • THIS.Log() schreibt ins Log des Dienstes, dasselbe, das Sie in der Konsole gesehen haben.
  • Es gibt kein TRY ... CATCH. Ein VFP-Fehler kommt von selbst ins Log, mit seiner Nummer und seiner Zeile, der Job zählt als fehlgeschlagen, und der Dienst läuft weiter.

Die Ersten Schritte im Handbuch (FoxForge > Hilfe > FoxService-Handbuch) gehen den Code jedes Jobs Zeile für Zeile durch: warum die CSV durch STRCONV() geht, warum das Datum mit EVALUATE("{^...}") gelesen wird und wie das SEEK verhindert, dass dieselbe Bestellung zweimal importiert wird.

Schritt 5. Von Hand ausprobieren

Mit dem Dienst wieder in der Konsole (Strg+F8) kopieren Sie die CSV aus dist\inbox\processed\ nach dist\inbox\. Das Log sagt:

INFO  job ImportOrders: orders_0001_20261012143307.csv: 0 new, 2 repeated

Der Job ist idempotent: Beide Bestellungen waren schon drin, und das SEEK nach id überspringt sie. Die Datei geht zurück nach processed\, mit der Uhrzeit im Namen, damit die vorige nicht überschrieben wird.

Die Zusammenfassung läuft um 23:55, aber Sie müssen nicht warten. FoxForge > Aktuelles Projekt > Einen Job jetzt ausführen… zeigt die Jobs mit dem Zeitpunkt, zu dem jeder läuft, die manuellen zuerst:

Das Fenster „Einen Job jetzt ausführen“ (auf Spanisch), mit RebuildSummary, dem manuellen Job, an erster Stelle und ausgewählt

Wählen Sie RebuildSummary und klicken Sie auf Ausführen. Läuft der Dienst, läuft der Job in ihm; sonst in einer Konsole, die mit dem Ergebnis offen bleibt. In dist\data\ erscheint summary_20261012.txt:

Orders: 2, total: 173.40

Und wenn etwas schiefgeht und Sie hineinschauen wollen, führt Aktuelles Projekt > Prüfstand… einen Job direkt aus dem Quelltext aus, ohne zu kompilieren, mit dem VFP-Debugger zur Hand: Ein SET STEP ON funktioniert.

Schritt 6. Einen Job hinzufügen: das Lager

Jetzt ein eigener. Jeden Morgen möchte das Lager eine CSV mit den offenen Bestellungen, und diese Bestellungen müssen als verschickt markiert werden, damit sie nicht noch einmal geschickt werden. Fügen Sie diese Methode in die Klasse ein, unter RebuildSummary:

Ein neuer Job: jeden Morgen die offenen Bestellungen ans Lager
    *-- Every morning: the pending orders, to a CSV for the warehouse,
    *-- and marked as sent.
    *!* @daily 07:00
    PROCEDURE SendToWarehouse
        LOCAL lnPending, lcFile
        THIS.OpenOrders()
        COUNT FOR status == "P" TO lnPending
        IF lnPending = 0
            THIS.Log("Nothing to send to the warehouse")
            RETURN
        ENDIF
        lcFile = THIS.Path("data") + "warehouse_" + TTOC(DATETIME(), 1) + ".csv"
        COPY TO (lcFile) FIELDS id, customer, amount FOR status == "P" TYPE CSV
        REPLACE ALL status WITH "S" FOR status == "P"
        THIS.Log(TRANSFORM(lnPending) + " order(s) sent to the warehouse: " + lcFile)
    ENDPROC

Die Namen und die Kommentare sind auf Englisch, wie der Rest des Beispiels; in Ihren Diensten verwenden Sie Ihre eigenen.

  • @daily 07:00 ist im 24-Stunden-Format, nach der Uhr des Rechners, auf dem der Dienst läuft. Ein @daily 7:00 AM erreicht den Server nicht: Strg+F7 hält den Build an und zeigt die Zeile, main.prg(134): error: @daily needs a local time HH:MM from 00:00 to 23:59.
  • Er fängt mit OpenOrders() an, wie die anderen: die Tabelle geöffnet und ausgewählt, also arbeiten COUNT und REPLACE auf ihr.
  • Die Uhrzeit steht im Dateinamen (TTOC(DATETIME(), 1)), weil COPY TO ohne Nachfrage überschreibt, auch mit SET SAFETY ON. Mit dem Datum allein würde ein zweiter Lauf am selben Tag die Datei vom Morgen überschreiben.
  • COPY TO ... TYPE CSV schreibt in Windows-1252, wie die Tabelle: „Ferretería“ kommt mit seinem Akzent heraus.

Strg+F7, und ohne bis sieben Uhr zu warten: Einen Job jetzt ausführen… > SendToWarehouse. Das Log sagt:

INFO  job SendToWarehouse: 2 order(s) sent to the warehouse: C:\a1\tutorial-svc\pedidos\dist\data\warehouse_20261012143501.csv
id,customer,amount
1001,"Ferretería Ruiz",125.40
1002,"Bar Manolo",48.00

Führen Sie ihn noch einmal aus: „Nothing to send to the warehouse“. Beide Bestellungen sind schon markiert, und der Zähler der offenen sagt in seiner nächsten Runde 0.

Schritt 7. Ihn als Windows-Dienst installieren

FoxForge > Aktuelles Projekt > Windows-Dienst… zeigt den Zustand des Dienstes und die Schaltflächen, um ihn zu bedienen. Klicken Sie auf Installieren: Windows fragt nach Administratorrechten, und der Dienst ist installiert und läuft. Ab jetzt startet er von selbst beim Hochfahren des Rechners, auch wenn sich niemand anmeldet, und Windows startet ihn neu, wenn er abstürzt.

Das Fenster „Windows-Dienst“ (auf Spanisch) mit dem laufenden Dienst pedidos, dem Konto LocalSystem, den aktiven Schaltflächen Anhalten und Deinstallieren, dem, was status sagt, und darunter einer Zeile pro Job mit Läufen, Fehlern, übersprungenen Runden und letztem Start

Unter den Schaltflächen zeigt das Fenster, was status sagt (das Konto, den automatischen Start, was Windows tut, wenn er abstürzt), und weiter unten eine Zeile pro Job: wie oft er gelaufen, fehlgeschlagen oder übersprungen worden ist und wann er zuletzt gestartet ist. Kopieren Sie eine CSV nach dist\inbox\ und sehen Sie sie im Log, jetzt ohne Konsole: Es liegt in dist\logs\pedidos_20261012.log, eines pro Tag.

Um erneut zu kompilieren, halten Sie ihn zuerst an, mit Anhalten: Windows lässt die Dateien eines laufenden Dienstes nicht ersetzen, und wenn Sie es vergessen, erinnert Strg+F7 Sie daran. Kompilieren, und Starten.

Schritt 8. Ihn auf einen Server bringen

Der Ordner dist\ enthält alles, was der Dienst braucht, die VFP-Laufzeit eingeschlossen. Auf dem Server braucht es weder Visual FoxPro noch eine Lizenz; nur Windows mit der 32-Bit-Laufzeit von .NET 10. Kopieren Sie dist\ vollständig auf eine lokale Platte des Servers und geben Sie in einer Administrator-Konsole in diesem Ordner ein:

pedidos.exe install --start

Das installiert ihn mit dem Konto aus foxservice.json (dem des Systems, das Sie beim Anlegen gewählt haben) und startet ihn. pedidos.exe status sagt, wie es um ihn steht, und pedidos.exe uninstall entfernt ihn und lässt die Logs liegen. Wenn der Dienst auf dem Server einen Netzwerkordner erreichen muss, erklärt das Handbuch, welches Konto Sie nehmen.

Schritt 9. Die Lizenz

Ohne Lizenz funktioniert der Dienst genauso: Alle Jobs laufen, ohne Einschränkungen und ohne Ablaufdatum. Das Einzige, was sich ändert: Er sagt es einmal beim Start, mit einer Zeile in seinem Log und einem Eintrag in der Ereignisanzeige:

WARN  Built with FoxCli (evaluation): this service runs every job, without limits. A FoxCli license seals it: https://irwinrodriguez.dev/foxcli

Mit einer FoxCli-Lizenz, geladen unter FoxForge > FoxCli-Lizenz…, siegelt Strg+F7 den Dienst mit Ihrem Namen: Diese Zeile verschwindet, und das Log und status sagen, an wen er lizenziert ist. Die Lizenz gehört dem Programmierer, der kompiliert: Die Server, auf denen Sie Ihre Dienste installieren, brauchen keine.

Wie es weitergeht

  • Das FoxService-Handbuch finden Sie unter FoxForge > Hilfe > FoxService-Handbuch: was in einem Job leben kann und was nicht, Timeouts und Quarantäne, die Windows-Konten, @watch im Detail, PDF-Berichte und VFP Advanced.
  • Mit einem Agenten: FoxStack gibt Claude die Befehle von foxservice als Werkzeuge, um Dienste aus einem Gespräch heraus anzulegen, zu kompilieren und zu installieren.
  • Ideen, wie es mit pedidos weitergeht: dass SendToWarehouse die CSV per E-Mail schickt, statt sie in data\ zu lassen, oder ein zweites @watch für einen Ordner mit Retouren.