Compare commits

..

16 Commits

Author SHA1 Message Date
github-actions[bot]
8c18978460 🔄 Update image to 3 [CI SKIP] 2025-11-05 14:49:03 +00:00
A'zamov Samandar
9fd19b8f7c fix typo
All checks were successful
Deploy to Production / build-and-deploy (push) Successful in 1m6s
2025-11-05 19:48:04 +05:00
A'zamov Samandar
b69a128a76 fix typo
All checks were successful
Deploy to Production / build-and-deploy (push) Successful in 1m5s
2025-11-05 19:43:16 +05:00
A'zamov Samandar
2bbe34f703 feat(devops)
Some checks failed
Deploy to Production / build-and-deploy (push) Failing after 15s
2025-11-05 19:41:57 +05:00
A'zamov Samandar
4267dec319 feat(devops): stack file created 2025-11-05 19:25:42 +05:00
A'zamov Samandar
d8f25ed430 .gitignore: main va release papkalarini qo'shdi 2025-04-26 17:41:52 +05:00
A'zamov Samandar
53bcbe16be .gitignore updated 2025-04-25 18:05:14 +05:00
A'zamov Samandar
83be69c277 systemd file created 2025-04-25 17:36:36 +05:00
A'zamov Samandar
1209499f6e fix: Token management update and SMS message type change 2025-04-25 15:31:10 +05:00
A'zamov Samandar
bd8fba1d88 feat: Yangilangan README, ko'p kanalli va brokerli xabar yetkazib beruvchi xizmat uchun. 2025-04-25 11:38:53 +05:00
A'zamov Samandar
130f03f973 Ovozli xabarlarni chiqarish funksiyasi qo'shildi va test email manzili yangilandi. 2025-04-25 11:25:15 +05:00
A'zamov Samandar
fbd002d8d4 fix typo 2025-04-25 10:27:49 +05:00
A'zamov Samandar
71634fc19e message broker sifatida redis qo'shildi 2025-04-25 10:25:49 +05:00
A'zamov Samandar
40200a4649 notification serviceda email xabar yuborish qo'shildi 2025-04-24 23:26:46 +05:00
A'zamov Samandar
e79718a3ea Merge commit '5032b6088fbe04c2dd2213db3982bdafdc806bd7' into dev 2025-04-21 20:53:16 +05:00
Samandar Azamov
f38471756c Update README.MD 2025-04-21 20:52:34 +05:00
20 changed files with 497 additions and 140 deletions

View File

@@ -1,15 +1,23 @@
RABBITMQ_URL=amqp://guest:guest@127.0.0.1:5672/
REDIS_ADDRESS=127.0.0.1:6379
REDIS_ADDRESS=redis:6379
REDIS_PASSWORD=
REDIS_DB=0
BROKER=redis
TOPIC=notification
ESKIZ_DOMAIN="https://notify.eskiz.uz/api"
ESKIZ_USER="admin@gmail.com"
ESKIZ_PASSWORD="password"
ESKIZ_FROM="4546"
MAIL_DOMAIN=smtp.gmail.com
MAIL_USER="JscorpTech@gmail.com"
MAIL_PASSWORD="app password"
MAIL_PORT=587
PMB_DOMAIN=""
PMB_USER=""
PMB_PASSWORD=""

98
.github/workflows/deploy.yaml vendored Normal file
View File

@@ -0,0 +1,98 @@
name: Deploy to Production
on:
push:
branches:
- main
env:
PROJECT_NAME: taxi-notification
permissions:
contents: write
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Copy env
run: |
cp .env.example .env
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build Docker image
uses: docker/build-push-action@v5
with:
context: .
file: Dockerfile
push: false
load: true
tags: ${{ env.PROJECT_NAME }}:test
no-cache: true
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Tag and push to Docker Hub
run: |
docker tag ${{ env.PROJECT_NAME }}:test ${{ secrets.DOCKER_USERNAME }}/${{ env.PROJECT_NAME }}:latest
docker tag ${{ env.PROJECT_NAME }}:test ${{ secrets.DOCKER_USERNAME }}/${{ env.PROJECT_NAME }}:${{ github.run_number }}
docker push ${{ secrets.DOCKER_USERNAME }}/${{ env.PROJECT_NAME }}:latest
docker push ${{ secrets.DOCKER_USERNAME }}/${{ env.PROJECT_NAME }}:${{ github.run_number }}
echo "SUCCESS TAGS: latest, ${{ github.run_number }}"
- name: Update stack.yaml and version
run: |
sed -i 's|image: ${{ secrets.DOCKER_USERNAME }}/${{ env.PROJECT_NAME }}:.*|image: ${{ secrets.DOCKER_USERNAME }}/${{ env.PROJECT_NAME }}:${{ github.run_number }}|' stack.yaml
- name: Commit and push updated version
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add .
git commit -m "🔄 Update image to ${{ github.run_number }} [CI SKIP]" || echo "No changes"
git pull origin main --rebase
git push origin main
- name: Deploy to server via SSH
uses: appleboy/ssh-action@v1.2.2
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
# key: ${{ secrets.KEY }}
password: ${{ secrets.PASSWORD }}
port: ${{ secrets.PORT }}
script: |
PROJECTS=/opt/projects/
DIR=/opt/projects/${{ env.PROJECT_NAME }}/
if [ -d "$PROJECTS" ]; then
echo "projects papkasi mavjud"
else
mkdir -p $PROJECTS
echo "projects papkasi yaratildi"
fi
if [ -d "$DIR" ]; then
echo "loyiha mavjud"
else
cd $PROJECTS
git clone git@gitea.felixits.uz:${{ github.repository }}.git ${{ env.PROJECT_NAME }}
echo "Clone qilindi";
fi
cd $DIR
git fetch origin main
git reset --hard origin/main
docker stack deploy -c stack.yaml ${{ env.PROJECT_NAME }}

5
.gitignore vendored
View File

@@ -1,3 +1,4 @@
.env
./bin
./main
bin
main
release

View File

@@ -15,6 +15,5 @@ FROM alpine
WORKDIR /app
COPY --from=build /app/notification .
COPY ./.env /app/
CMD ["./notification"]

189
README.MD
View File

@@ -1,133 +1,170 @@
# Notification Service
A microservice for handling and delivering notifications through various channels like SMS and email using RabbitMQ as a message broker.
## Overview
This notification service is designed as a standalone microservice that consumes notification requests from a RabbitMQ queue and routes them to the appropriate notification provider based on the notification type. Currently, it supports SMS and email notifications.
A flexible notification service that supports multiple message brokers (Redis, RabbitMQ) and notification channels (SMS, Email). This service is designed to handle asynchronous notification delivery in your applications.
## Features
- Message consumption from RabbitMQ
- Support for multiple notification channels (SMS, email)
- Extensible architecture for adding new notification types
- Asynchronous notification handling
- **Multiple Brokers**: Support for both Redis and RabbitMQ as message brokers
- **Multiple Notification Channels**: SMS and Email notification support
- **Containerized**: Ready to deploy with Docker
- **Extensible Architecture**: Easy to add new notification channels or message brokers
## Architecture
The notification service follows a clean architecture approach:
The service follows a clean architecture pattern with the following components:
- **Domain Layer**: Contains core business logic and port interfaces
- **Infrastructure Layer**: Implements the ports with concrete adapters
- **RabbitMQ**: Used as a message broker for consuming notification requests
- **Broker**: Handles message subscription from different sources (Redis/RabbitMQ)
- **Notifier**: Implements different notification channels (SMS/Email)
- **Services**: Contains the business logic for each notification channel
- **Domain**: Defines interfaces and data models
## Prerequisites
- Go 1.24 or higher
- Redis (for Redis broker)
- RabbitMQ (for RabbitMQ broker)
- Docker (optional, for containerized deployment)
## Configuration
Copy the provided `.env.example` to `.env` and update with your configuration:
```bash
cp .env.example .env
```
### Environment Variables
| Variable | Description | Example |
|----------|-------------|---------|
| BROKER | Message broker to use (redis or rabbitmq) | redis |
| TOPIC | Topic/queue name for notifications | notification |
| REDIS_ADDRESS | Redis server address | 127.0.0.1:6379 |
| REDIS_PASSWORD | Redis password (if any) | |
| REDIS_DB | Redis database number | 0 |
| RABBITMQ_URL | RabbitMQ connection URL | amqp://guest:guest@localhost:5672/ |
| ESKIZ_DOMAIN | Eskiz SMS API domain | https://notify.eskiz.uz/api |
| ESKIZ_USER | Eskiz SMS API username | admin@example.com |
| ESKIZ_PASSWORD | Eskiz SMS API password | password |
| ESKIZ_FROM | Eskiz SMS sender ID | 4546 |
| MAIL_DOMAIN | SMTP server domain | smtp.gmail.com |
| MAIL_USER | SMTP username | notification@example.com |
| MAIL_PASSWORD | SMTP password | yourpassword |
| MAIL_PORT | SMTP port | 587 |
## Installation
### Prerequisites
### Local Development
- Go 1.x+
- RabbitMQ server
### Setup
1. Clone the repository:
1. Clone the repository
```bash
git clone https://github.com/JscorpTech/notification.git
cd notification
```
2. Install dependencies:
2. Install dependencies
```bash
go mod download
```
3. Build the application:
3. Build and run the application
```bash
go build -o notification-service
go build -o notification ./cmd/main.go
./notification
```
## Configuration
### Docker Deployment
Configure your RabbitMQ connection and other settings in the appropriate configuration files.
Build and run using Docker:
```bash
docker build -t notification-service .
docker run -p 8080:8080 --env-file .env notification-service
```
Or using Docker Compose:
```bash
docker-compose up -d
```
## Usage
### Running the service
### Message Format
```bash
./notification-service
```
This will start the notification consumer that listens for incoming notification requests.
### Sending a notification
Notifications should be published to the RabbitMQ exchange with the following JSON format:
The service expects messages in the following JSON format:
```json
{
"type": "email",
"message": "Hello, this is a test notification.",
"message": "Subject: Welcome\r\n\r\nHello, welcome to our service.",
"to": ["user@example.com"]
}
```
Python example
For SMS notifications:
```json
{
"type": "sms",
"message": "Your verification code is 1234",
"to": ["+998901234567"]
}
```
### Sending Messages
#### Using Redis
```python
import redis
import json
r = redis.StrictRedis(host='127.0.0.1', port=6379, db=0)
message = {
'type': 'email',
'message': "Subject: Welcome\r\n\r\nWelcome to our service!",
'to': ["user@example.com"]
}
r.rpush('notification', json.dumps(message))
```
#### Using RabbitMQ
```python
from kombu import Connection, Exchange, Producer
# RabbitMQ ulanishi
rabbit_url = 'amqp://guest:guest@127.0.0.1:5672/'
connection = Connection(rabbit_url)
channel = connection.channel()
exchange = Exchange('notification', type='direct')
# Producer yaratish
producer = Producer(channel, exchange=exchange, routing_key="notification")
# Xabar yuborish
message = {'type': 'sms', 'message': "classcom.uz sayti va mobil ilovasiga ro'yxatdan o'tishingingiz uchun tasdiqlash kodi: 1234", "to": ["+998888112309", "+998943990509"]}
message = {
'type': 'sms',
'message': "Your verification code is 1234",
'to': ["+998901234567"]
}
producer.publish(message)
print("Message sent to all workers!")
```
Available notification types:
- `email`: For email notifications
- `sms`: For SMS notifications
## Adding New Notification Channels
## Project Structure
1. Create a new notifier implementation in `internal/notifier/`
2. Implement the `domain.NotifierPort` interface
3. Add the new notifier type to the `Handler` function in `internal/consumer/notification.go`
```
notification/
├── cmd/
│ └── main.go # Entry point
├── internal/
│ ├── domain/
│ │ └── ports.go # Interfaces
│ ├── notifier/
│ │ ├── email.go # Email notification implementation
│ │ └── sms.go # SMS notification implementation
│ ├── rabbitmq/
│ │ └── connection.go # RabbitMQ connection handling
│ └── consumer/
│ └── consumer.go # Implementation of the notification consumer
└── README.md
```
## License
MIT License - See [LICENSE](LICENSE) file for details.
## Contributing
1. Fork the repository
2. Create your feature branch: `git checkout -b feature/my-new-feature`
3. Commit your changes: `git commit -am 'Add some feature'`
4. Push to the branch: `git push origin feature/my-new-feature`
5. Submit a pull request
## License
[Add your license here]
## Contact
JscorpTech - [GitHub](https://github.com/JscorpTech)
2. Create a feature branch (`git checkout -b feature/my-feature`)
3. Commit your changes (`git commit -am 'Add some feature'`)
4. Push to the branch (`git push origin feature/my-feature`)
5. Create a new Pull Request

View File

@@ -2,6 +2,7 @@ package main
import (
"context"
"log"
"github.com/JscorpTech/notification/internal/consumer"
"github.com/JscorpTech/notification/internal/redis"
@@ -12,7 +13,7 @@ var ctx = context.Background()
func main() {
if err := godotenv.Load(); err != nil {
panic(err)
log.Println(".env not load")
}
redis.InitRedis()
notification := consumer.NewNotificationConsumer(ctx)

View File

@@ -0,0 +1,46 @@
package broker
import (
"context"
"encoding/json"
"fmt"
"log"
"github.com/JscorpTech/notification/internal/domain"
"github.com/JscorpTech/notification/internal/rabbitmq"
)
type rabbitMQBroker struct {
Ctx context.Context
}
func NewRabbitMQBroker(ctx context.Context) domain.BrokerPort {
return &rabbitMQBroker{
Ctx: ctx,
}
}
func (r rabbitMQBroker) Subscribe(topic string, handler func(domain.NotificationMsg)) {
conn, ch, err := rabbitmq.Connect()
if err != nil {
log.Fatal(err)
}
defer conn.Close()
defer ch.Close()
ch.ExchangeDeclare(topic, "direct", true, false, false, false, nil)
q, _ := ch.QueueDeclare(topic, true, false, false, false, nil)
ch.QueueBind(q.Name, topic, topic, false, nil)
msgs, _ := ch.Consume(q.Name, "", true, false, false, false, nil)
go func() {
for msg := range msgs {
var notification domain.NotificationMsg
if err := json.Unmarshal(msg.Body, &notification); err != nil {
fmt.Print(err.Error())
}
go handler(notification)
}
}()
}

39
internal/broker/redis.go Normal file
View File

@@ -0,0 +1,39 @@
package broker
import (
"context"
"encoding/json"
"fmt"
"github.com/JscorpTech/notification/internal/domain"
"github.com/JscorpTech/notification/internal/redis"
)
type redisBroker struct {
Ctx context.Context
}
func NewRedisBroker(ctx context.Context) domain.BrokerPort {
return &redisBroker{
Ctx: ctx,
}
}
func (r redisBroker) Subscribe(topic string, handler func(domain.NotificationMsg)) {
go func() {
for {
var notification domain.NotificationMsg
val, err := redis.RDB.BLPop(r.Ctx, 0, topic).Result()
if err != nil {
fmt.Print(err.Error())
return
}
if err := json.Unmarshal([]byte(val[1]), &notification); err != nil {
fmt.Print(err.Error())
return
}
go handler(notification)
}
}()
}

View File

@@ -2,14 +2,12 @@ package consumer
import (
"context"
"encoding/json"
"fmt"
"log"
"os"
"github.com/JscorpTech/notification/internal/broker"
"github.com/JscorpTech/notification/internal/domain"
"github.com/JscorpTech/notification/internal/notifier"
"github.com/JscorpTech/notification/internal/rabbitmq"
"github.com/streadway/amqp"
)
type notificationConsumer struct {
@@ -23,39 +21,26 @@ func NewNotificationConsumer(ctx context.Context) domain.NotificationConsumerPor
}
func (n *notificationConsumer) Start() {
conn, ch, err := rabbitmq.Connect()
if err != nil {
log.Fatal(err)
brokerName := os.Getenv("BROKER")
if brokerName == "" {
brokerName = "redis"
}
defer conn.Close()
defer ch.Close()
exchangeName := "notification"
queueName := "notification"
routingKey := "notification"
ch.ExchangeDeclare(exchangeName, "direct", true, false, false, false, nil)
q, _ := ch.QueueDeclare(queueName, true, false, false, false, nil)
ch.QueueBind(q.Name, routingKey, exchangeName, false, nil)
msgs, _ := ch.Consume(q.Name, "", true, false, false, false, nil)
go func() {
for msg := range msgs {
go n.Handler(msg)
var brokerService domain.BrokerPort
switch brokerName {
case "redis":
brokerService = broker.NewRedisBroker(n.Ctx)
case "rabbitmq":
brokerService = broker.NewRabbitMQBroker(n.Ctx)
default:
brokerService = broker.NewRedisBroker(n.Ctx)
}
}()
brokerService.Subscribe(os.Getenv("TOPIC"), n.Handler)
fmt.Println("🚀 Server started. Ctrl+C to quit.")
select {}
}
func (n *notificationConsumer) Handler(msg amqp.Delivery) {
var notification domain.NotificationMsg
err := json.Unmarshal(msg.Body, &notification)
if err != nil {
fmt.Print(err.Error())
}
func (n *notificationConsumer) Handler(notification domain.NotificationMsg) {
var ntf domain.NotifierPort
switch notification.Type {
case "sms":
@@ -63,5 +48,6 @@ func (n *notificationConsumer) Handler(msg amqp.Delivery) {
case "email":
ntf = notifier.NewEmailNotifier()
}
fmt.Println(notification.Message)
ntf.SendMessage(notification.To, notification.Message)
}

View File

@@ -0,0 +1,6 @@
package domain
type BrokerPort interface {
Subscribe(string, func(NotificationMsg))
// Publish()
}

5
internal/domain/email.go Normal file
View File

@@ -0,0 +1,5 @@
package domain
type EmailServicePort interface {
SendMail([]string, []byte)
}

View File

@@ -1,10 +1,8 @@
package domain
import "github.com/streadway/amqp"
type NotificationConsumerPort interface {
Start()
Handler(amqp.Delivery)
Handler(NotificationMsg)
}
type SMSServicePort interface {

View File

@@ -2,15 +2,19 @@ package notifier
import (
"github.com/JscorpTech/notification/internal/domain"
"github.com/k0kubun/pp/v3"
"github.com/JscorpTech/notification/internal/services"
)
type emailNotifier struct{}
type emailNotifier struct {
EmailService domain.EmailServicePort
}
func NewEmailNotifier() domain.NotifierPort {
return &emailNotifier{}
return &emailNotifier{
EmailService: services.NewEmailService(),
}
}
func (n *emailNotifier) SendMessage(to []string, body string) {
pp.Print(to, body)
n.EmailService.SendMail(to, []byte(body))
}

View File

@@ -0,0 +1,33 @@
package services
import (
"fmt"
"net/smtp"
"os"
"github.com/JscorpTech/notification/internal/domain"
)
type emailService struct{}
func NewEmailService() domain.EmailServicePort {
return &emailService{}
}
func (e *emailService) SendMail(to []string, body []byte) {
// Gmail konfiguratsiyasi
from := os.Getenv("MAIL_USER")
password := os.Getenv("MAIL_PASSWORD")
smtpHost := os.Getenv("MAIL_DOMAIN")
smtpPort := os.Getenv("MAIL_PORT")
auth := smtp.PlainAuth("", from, password, smtpHost)
err := smtp.SendMail(smtpHost+":"+smtpPort, auth, from, to, body)
if err != nil {
fmt.Println("Xatolik:", err)
return
}
fmt.Println("Email yuborildi!")
}

View File

@@ -4,6 +4,7 @@ import (
"bytes"
"context"
"encoding/json"
"log"
"net/http"
"os"
"time"
@@ -35,7 +36,7 @@ func (e *eskizSMSService) Request(payload any, path string, isAuth bool, retry b
req, err := http.NewRequest("POST", e.BaseURL+path, &buf)
req.Header.Add("Content-Type", "application/json")
if isAuth {
req.Header.Add("Authorization", "Bearer "+e.GetToken(true))
req.Header.Add("Authorization", "Bearer "+e.GetToken(true, true))
}
if err != nil {
@@ -43,24 +44,32 @@ func (e *eskizSMSService) Request(payload any, path string, isAuth bool, retry b
}
res, err := client.Do(req)
if res.StatusCode == http.StatusUnauthorized && retry {
time.Sleep(time.Second * 5)
pp.Print("Qayta urunish")
e.GetToken(false)
e.GetToken(false, false)
return e.Request(payload, path, isAuth, false)
}
return res, err
}
func (e *eskizSMSService) GetToken(cache bool) string {
func (e *eskizSMSService) GetToken(cache bool, retry bool) string {
email := os.Getenv("ESKIZ_USER")
password := os.Getenv("ESKIZ_PASSWORD")
if email == "" || password == "" {
log.Fatal("password or fmail not found")
}
token, err := redis.RDB.Get(e.Ctx, "eskiz_token").Result()
if err == nil && cache {
pp.Print("Eskiz token topildi 😁")
return token
}
payload := domain.EskizLogin{
Email: os.Getenv("ESKIZ_USER"),
Password: os.Getenv("ESKIZ_PASSWORD"),
Email: email,
Password: password,
}
res, err := e.Request(payload, "/auth/login", false, true)
res, err := e.Request(payload, "/auth/login", false, retry)
if err != nil {
pp.Print(err.Error())
}

15
notify.service Normal file
View File

@@ -0,0 +1,15 @@
[Unit]
Description="Notification service"
After=network.target
[Service]
User=root
Group=root
Type=simple
Restart=on-failure
RestartSec=5s
ExecStart=/home/user/projects/notification/main
WorkingDirectory=/home/user/projects/notification
[Install]
WantedBy=multi-user.target

35
stack.yaml Normal file
View File

@@ -0,0 +1,35 @@
version: "3.8"
services:
notification:
image: jscorptech/taxi-notification:3
env_file:
- /opt/env/.notification
networks:
- taxi
deploy:
mode: replicated
restart_policy:
condition: any
update_config:
parallelism: 1
order: start-first
failure_action: rollback
monitor: 10s
delay: 10s
max_failure_ratio: 0.2
resources:
limits:
cpus: "2"
memory: 1024M
logging:
driver: json-file
options:
max-size: "10m"
max-file: 10
networks:
taxi:
driver: overlay
external: true
attachable: true

36
test.py Normal file
View File

@@ -0,0 +1,36 @@
# from kombu import Connection, Exchange, Producer
# # RabbitMQ ulanishi
# rabbit_url = 'amqp://guest:guest@127.0.0.1:5672/'
# connection = Connection(rabbit_url)
# channel = connection.channel()
# exchange = Exchange('notification', type='direct')
# # Producer yaratish
# producer = Producer(channel, exchange=exchange, routing_key="notification")
# # Xabar yuborish
# message = {'type': 'email', 'message': "Subject: test\r\n\r\nclasscom.uz sayti va mobil ilovasiga ro'yxatdan o'tishingingiz uchun tasdiqlash kodi: 1234", "to": ["JscorpTech@gmail.com", "admin@jscorp.uz"]}
# producer.publish(message)
# print("Message sent to all workers!")
import redis
import json
# Redis ulanishi
r = redis.StrictRedis(host='127.0.0.1', port=6379, db=0)
# Subject: tasdiqlash ko'di\r\n\r\n
# Xabar tayyorlash
message = {
'type': 'sms',
'message': "Assalomu alaykum samandar sizni https://classcom.uz oqituvchining virtual kаbinetida muallif sifatida tasdiqlanganingiz bilan tabriklaymiz!!!",
'to': ["+998888112309"]
}
# Xabarni JSON formatga otkazib, Redis listga push qilish
r.rpush('notification', json.dumps(message))
print("Message pushed to Redis list!")

2
tmp/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
*
!.gitignore

View File

@@ -1 +0,0 @@
exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1