בואו נכתוב מסוף עם Python, React ו MobX

פוסט זה כולל טיפ קצר לעבודה יעילה עם Python. אם אתם רוצים ללמוד פייתון יותר לעומק אני ממליץ על קורס Python כאן באתר.
הקורס כולל עשרות שיעורי וידאו והמון תרגול מעשי וילמד אתכם Python בצורה מקצועית מההתחלה ועד הנושאים המתקדמים.

התמיכה המצוינת של פייתון ב Socket.IO מאפשרת לנו במעט מאוד קוד לכתוב ממשקים גרפיים לכלי שורת פקודה. בדוגמה של היום אני רוצה לכתוב ממשק גרפי להרצה של פקודות על מכונה מרוחקת. יהיה לנו שרת פייתון שמקבל הודעות ומריץ פקודות, את כל הפלט של הפקודה הוא שולח ב Web Socket ליישום צד לקוח להצגה על המסך.

1. קוד צד שרת - פייתון ו Socket.IO

נתקין את הספריה python-socketio וגם את aiohttp, שתיהן עם pip:

$ pip install aiohttp python-socketio

ואנחנו מוכנים לכתוב קוד צד שרת. בשביל שרת Socket.IO בפייתון מספיק לכתוב פונקציה לכל הודעה שאנחנו צריכים לקבל ולסמן אותה עם ה Decorator המתאים. אני רוצה לקבל הודעה שנקראת start ושיהיה לה פרמטר שהוא הטקסט של שורת הפקודה להרצה. פייתון יריץ את הפקודה ויקרא שורות מ stdout, וכל שורה שהוא קורא הוא ישלח חזרה לצד לקוח. בשביל זה מספיק לי קובץ פייתון אחד אני קורא לו server.py עם התוכן הבא:

from aiohttp import web
import asyncio
import socketio

sio = socketio.AsyncServer(cors_allowed_origins='*', aync_mode='aiohttp')
app = web.Application()

sio.attach(app)

@sio.on('start')
async def start_command(sid, cmd):
    print("Socket ID: " , sid)
    proc = await asyncio.create_subprocess_shell(
        cmd,
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE)

    while not proc.stdout.at_eof():
        next_line = await proc.stdout.readline()
        await sio.emit('output', next_line, to=sid)

    await proc.wait()

if __name__ == '__main__':
    web.run_app(app)

שתי פונקציות התקשורת כאן הן sio.on שמחברת קוד טיפול באירוע לאירוע עם שם מסוים (בדוגמה שלנו start) ו sio.emit ששולחת אירוע ללקוח. אני משתמש בפרמטר to כדי לשלוח את הפלט רק ללקוח שהריץ את הפקודה.

2. קוד צד לקוח - מובאקס

בצד הלקוח אני רוצה לכתוב מחלקה של לוגיקה שיודעת להריץ פקודות ולשמור את הפלט שלהן. יהיה לי נוח לכתוב את הלוגיקה במובאקס עם מערך ששומר את כל שורות הפלט. כל פעם שמגיעה שורת פלט חדשה נכתוב אותה למערך, וכשמריצים פקודה חדשה נרוקן את המערך. את הקוד כתבתי בקובץ בשם runner.js בתוך תיקיה בשם mobx. זה תוכן הקובץ:

import { observable, makeObservable, action } from 'mobx';
import { io } from "socket.io-client";

const socket = io("http://localhost:8080");

const enc = new TextDecoder();


class Runner {
  constructor() {
    this.output = [];
    makeObservable(this, {
      clear: action.bound,
      append: action.bound,
      output: observable,
    });
  }

  append(msg) {
    const msgText = enc.decode(msg);
    this.output.push(msgText);
  }

  clear() {
    this.output = [];
  }

  start(cmd) {
    this.clear();
    socket.emit('start', cmd);
  }
}

const runner = new Runner();
socket.on('output', (msg) => {
  runner.append(msg);
});
export default runner;

הפונקציה היחידה ששווה להתעכב עליה כאן היא append וספציפית על שורת ההמרה בה:

const enc = new TextDecoder();

// ...
const msgText = enc.decode(msg);

אנחנו מקבלים הודעות מפייתון בתור רצפים של בתים ולא בתור מחרוזות, בגלל שככה עובד ה API של הרצת פקודות מערכת בפייתון. ב JavaScript, צריך לתרגם את אוסף הבתים הזה למחרוזת וזה התפקיד של TextDecoder. אפשר היה גם לעשות את ההמרה בפייתון לפני השליחה אם אתם מרגישים יותר נוח שם.

3. הצגת הפלט בממשק ריאקט

והחלק האחרון הוא קוד ריאקט שמציג תיבה לכתיבת פקודה וקופסה שבה מוצג כל הפלט. משתמש כותב פקודה בתיבה, שרת פייתון מריץ אותה ושולח את הפלט חזרה והשתמש מקבל את כל הפלט לתיבת ה output. זה הקוד בקובץ App.jsx:

import { useRef } from 'react'
import { observer } from 'mobx-react-lite';
import runner from './mobx/runner';
import './index.css';

const Output = observer(function Output() {
  return (
    <div className="output">
      <ul>
        {runner.output.map((line, index) => (
          <li key={index}><pre>{line}</pre></li>
        ))}
      </ul>
    </div>
  );
});

const App = observer(function App() {
  const form = useRef(null);
  function start(ev) {
    ev.preventDefault();
    const cmd = form.current.querySelector('input').value;
    runner.start(cmd);
    form.current.querySelector('input').value = '';
  }

  return (
    <div className="App">
      <p>{JSON.stringify(runner.lines)}</p>
      <form onSubmit={start} ref={form}>
        <label>
          Command:
          <input type="text" />
        </label>
        <button>Start</button>
      </form>
      <Output />
    </div>
  )
});

export default App

וכמעט בלי קוד הצלחתי ליצור קשר דו כיווני בין קוד צד לקוח לשרת פייתון. עם הפופולריות של ממשקי ווב, מנגנון כזה יכול לספק תחליף טוב לכלי שורת פקודה או להתממשק לכלי שורת פקודה קיים שלכם.

4. עכשיו אתם

הקוד כרגע עובד ומאפשר להריץ פקודות על השרת וגם לעבוד מכמה לקוחות במקביל. הקוד לא מטפל במצב בו משתמש מנסה להריץ כמה פקודות במקביל - קוד צד הלקוח יציג את הפלט של שתי הפקודות מעורבב, וקוד צד השרת לא מספק כרגע שום דרך ללקוח להבדיל מה הפקודה שהוא הריץ שיצרה את הפלט שנשלח. תרגיל מעניין למי שרוצה לשחק עם הקוד הזה יהיה להוסיף את התמיכה, כלומר להוסיף למידע שהשרת שולח גם מזהה של הפקודה שהוא מריץ, ולעדכן את קוד צד הלקוח כך שאפשר יהיה לראות שורות פלט מכמה פקודות במקביל, אולי בצבעים שונים לפי המזהה של כל פקודה.