پس از استخدام در پارک علم و فناوری پردیس، یکی از غولهای تکنولوژی کشور در یک حرکت خلاقانه و در راستای خودکفایی تصمیم گرفته یک پیادهسازی فان از `Docker` با نام `Dockerious` توسعه دهد. این پروژه به عنوان پروژه شما در دوران آزمایشی تعیین شده. در فاز اول شرکتکنندگان باید بخش هستهای و ابتدایی این ابزار را پیادهسازی کنند.
شما باید یک ابزار خط فرمان *(CLI)* ساده بسازید که بتواند دستورات سیستمعامل را (البته از لیست مجاز قرار داده شده در `internal/commands/allowed.go`) در پسزمینه اجرا کرده و وضعیت آنها را ترک کند.
### جزئیات پروژه
پروژه اولیه را از [این لینک](/contest/assignments/84125/download_problem_initial_project/285903/) دانلود کنید. ساختار پروژه به شکل زیر است:
```Shell
├── cmd
│ ├── ps.go # TODO Implement
│ ├── root.go # TODO Implement
│ └── run.go # TODO Implement
├── go.mod
├── go.sum
├── internal
│ ├── commands
│ │ └── allowed.go
│ ├── db
│ │ └── database.go
│ ├── models
│ │ └── process.go
│ ├── monitor
│ │ └── monitor.go
│ └── types
│ └── status.go
├── main.go
```
در این پروژه، شما صرفا باید قسمتهای مشخص شده با `// TODO` را در دایرکتوری `cmd` پیادهسازی کنید. هدف اصلی، پیادهسازی منطق دو دستور اصلی `dockerious run` و `dockerious ps` است.
1. **`internal/commands/allowed.go`**: یک `map` حاوی لیست دستورات مجاز (`echo`, `ls`, `sleep`, `pwd`) را تعریف میکند.
2. **`internal/db/database.go`**: مسئولیت اتصال به پایگاهداده *PostgreSQL* را بر عهده دارد.
3. **`internal/models/process.go`**: مدل *GORM* برای جدول `processes` را تعریف میکند.
4. **`internal/types/status.go`**: وضعیتهای مختلف یک پروسه (`running`, `finished`, `failed`) را به صورت ثابت تعریف میکند.
5. **`internal/monitor/monitor.go`**: یک اینترفیس برای نظارت بر چرخه حیات پروسهها تعریف میکند. شما باید پیادهسازی آن را در `cmd/run.go` انجام دهید.
> ⚠️ نباید ساختار فایلهای داخل `internal` را تغییر دهید — فقط `cmd/*.go` قسمتهایی که `// TODO` دارند باید تکمیل شوند.
### مدل پایگاهداده
ساختار جدول `processes` در پایگاهداده که مدل آن در `models/process.go` تعریف شده، به شرح زیر است:
| فیلد | تایپ | توضیحات |
|-------------|-------------|-----------------------------------------------|
| `ID` | `uint` | کلید اصلی `AUTO_INCREMENT` |
| `PID` | `int` | شناسهی پروسه که سیستمعامل برمیگرداند |
| `Command` | `string` | دستور کامل اجرا شده |
| `Status` | `string` | وضعیت پروسه (`running`، `finished`، `failed`) |
| `CreatedAt` | `time.Time` | زمان ایجاد رکورد |
| `UpdatedAt` | `time.Time` | زمان آخرین بهروزرسانی رکورد |
|
| `DeletedAt` | `time.Time` | زمان حذف رکورد |
### دستورات خط فرمان
شما باید توابع و متدهای خالی مشخصشده با `// TODO` را در فایلهای **`cmd/root.go`**، **`cmd/run.go`** و **`cmd/ps.go`** کامل کنید.
<details class="blue"><summary>فایل `cmd/root.go`</summary>
**توضیح:**
این فایل مسئول راهاندازی ساختار اصلی CLI است. شما باید دستورات فرزند را به دستور ریشه متصل کرده و نقطهی ورود برنامه را تکمیل کنید.
**نیازمندیها:**
+ **`func init()`**: در این تابع، باید دستورات `runCmd` و `psCmd` را به عنوان فرزند به `rootCmd` اضافه کنید.
+ **`func Execute()`**: این تابع که از `main.go` فراخوانی میشود، باید `rootCmd` را اجرا کرده و خطاهای احتمالی را مدیریت کند.
+ **`func GetRootCmdForTest()`**: این تابع در تستها استفاده میشود و شما نباید آن را ویرایش کنید!
</details>
<details class="blue"><summary>فایل `cmd/run.go`</summary>
**توضیح:**
این فایل منطق اصلی اجرای یک پروسه جدید را در خود جای داده است.
**نیازمندیها:**
+ **`struct AsyncProcessMonitor`**: شما باید این `struct` را به گونهای پیادهسازی کنید که اینترفیس `monitor.ProcessMonitor` را ارضا کند.
+ **`func (dpm *AsyncProcessMonitor) MonitorProcess(...)`**: این متد باید یک **گوروتین** اجرا کند. این گوروتین باید منتظر بماند تا پروسهی ورودی (`proc`) تمام شود و سپس وضعیت رکورد متناظر با آن `pid` را در دیتابیس به `types.StatusFinished` تغییر دهد.
+ **`var runCmd = &cobra.Command{...}`**: در تابع `Run` این دستور، باید منطق کامل زیر را پیادهسازی کنید:
1. **بررسی Whitelist**: چک کنید که آیا فرمان ورودی در `commands.AllowedCommands` وجود دارد یا خیر. در صورت عدم وجود، اگر دستور مجاز نباشد، باید خطا را به شکل زیر چاپ کنید و هیچ رکوردی هم در دیتابیس ذخیره نشود:
```shell
[Dockerious] Error: Command '<command>' is not allowed.
```
2. **آرگومانها**: اگر بدون هیچ آرگومانی صدا زده شود، باید خطای پیشفرض (`requires at least 1 arg(s)`) بازگردانده شود.
3. **اجرای پروسه**: با استفاده از `os/exec` و متد `Start()`، پروسه را در پسزمینه اجرا کنید.
4. **ثبت در دیتابیس**: یک رکورد جدید برای پروسه در دیتابیس با وضعیت `types.StatusRunning` ایجاد کنید.
5. چاپ خروجی موفقیت: پس از اجرای موفق، پیام زیر چاپ شود:
```shell
[Dockerious] Started process "<command and args>" with PID: <pid>
```
6. **شروع نظارت**: با استفاده از `monitor.GetProcessMonitor()`، متد `MonitorProcess` را برای پروسهی جدید فراخوانی کنید تا وضعیت آن پس از اتمام، به صورت خودکار آپدیت شود.
</details>
<details class="blue"><summary>فایل `cmd/ps.go`</summary>
**توضیح:**
این دستور برای نمایش لیست پروسههای در حال اجرا و اصلاح وضعیت رکوردهای تاریخگذشته است.
**نیازمندیها:**
+ **`var psCmd = &cobra.Command{...}`**: در تابع `Run` این دستور، باید منطق کامل زیر را پیادهسازی کنید:
1. **واکشی از دیتابیس**: تمام پروسههایی که وضعیت آنها `types.StatusRunning` است را از دیتابیس بخوانید.
2. **منطق Reconciliation**: برای هر پروسه، با استفاده از `os.FindProcess` و ارسال سیگنال `0`، بررسی کنید که آیا واقعاً در سیستمعامل در حال اجراست یا خیر.
3. **آپدیت وضعیت**: اگر پروسهای در سیستمعامل وجود نداشت، وضعیت آن را در دیتابیس با فراخوانی تابع `updateStatusToFinished` بهروز کنید و دیگر در خروجی نمایش داده نشود.
4. **چاپ خروجی**: لیست نهایی پروسههایی که واقعاً در حال اجرا هستند را در قالب یک جدول مرتب چاپ کنید.
+ برای فرمت جدول میتوانید از خط زیر کمک بگیرید:
```go
w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', tabwriter.TabIndent)
fmt.Fprintln(w, "ID\tPID\tSTATUS\tCOMMAND")
```
خروجی باید همیشه با هدر زیر شروع شود:
`ID PID STATUS COMMAND `
و در ادامه هر رکورد یک پروسه نمایش داده شود.
+ **`func updateStatusToFinished(...)`**: این تابع کمکی باید یک رکورد پروسه را دریافت کرده و وضعیت آن را در دیتابیس به `types.StatusFinished` تغییر دهد.
</details>
### نکات
+ فایلهای `db.go` و `main.go` از قبل آماده شدهاند و صرفا برای اجرا در محیط لوکال میتوانید آنها را تغییر دهید.
+ تنها فایلهایی که نیاز به ویرایش دارند، فایلهای داخل پوشهی `cmd/` هستند.
### چه چیزی را آپلود کنید
پس از پیادهسازی ویژگیهای خواستهشده، **فقط** دایرکتوریهای `cmd` را فشرده کرده و به صورت یک فایل زیپ ارسال کنید.