Pastikan hanya satu contoh program yang berjalan


120

Apakah ada cara Pythonic agar hanya satu contoh dari program yang berjalan?

Satu-satunya solusi masuk akal yang saya temukan adalah mencoba menjalankannya sebagai server di beberapa port, lalu program kedua mencoba mengikat ke port yang sama - gagal. Tapi itu bukan ide yang bagus, mungkin ada sesuatu yang lebih ringan dari ini?

(Pertimbangkan bahwa program terkadang gagal, yaitu segfault - jadi hal-hal seperti "file kunci" tidak akan berfungsi)


1
Mungkin hidup Anda akan lebih mudah jika Anda melacak dan memperbaiki segfault tersebut. Bukannya itu hal yang mudah dilakukan.
David Locke

Itu tidak ada di perpustakaan saya, itu ada di libxml binding python dan sangat pemalu - hanya aktif sekali dalam beberapa hari.
Slava V

5
Pustaka standar Python mendukung flock (), yang merupakan Hal yang Benar untuk program UNIX modern. Membuka port menggunakan tempat di namespace yang jauh lebih terbatas, sedangkan pidfile lebih kompleks karena Anda perlu memeriksa proses yang sedang berjalan untuk membatalkannya dengan aman; kawanan tidak memiliki masalah.
Charles Duffy

s / UNIX / linux / ini dia, FTFY.
kaleissin

Jawaban:


100

Kode berikut harus melakukan pekerjaannya, ini lintas platform dan berjalan pada Python 2.4-3.2. Saya mengujinya di Windows, OS X dan Linux.

from tendo import singleton
me = singleton.SingleInstance() # will sys.exit(-1) if other instance is running

Versi kode terbaru tersedia singleton.py . Silakan laporkan bug di sini .

Anda dapat menginstal cenderung menggunakan salah satu metode berikut:


2
Saya memperbarui jawabannya dan menyertakan tautan ke versi terbaru. Jika Anda menemukan bug, silakan kirimkan ke github dan saya akan menyelesaikannya sesegera mungkin.
sorin

2
@Johny_M Terima kasih, saya membuat tambalan dan merilis versi yang lebih baru di pypi.python.org/pypi/tendo
sorin

2
Sintaks ini tidak berfungsi untuk saya di windows dengan Python 2.6. Apa yang berhasil bagi saya adalah: 1: from tendo import singleton 2: me = singleton.SingleInstance ()
Brian

25
Ketergantungan lain untuk sesuatu yang sepele seperti ini? Kedengarannya tidak menarik.
WhyNotHugo

2
Apakah singleton menangani proses yang mendapatkan sigterm (misalnya jika suatu proses berjalan terlalu lama), atau apakah saya harus mengatasinya?
JimJty

43

Sederhana, cross-platform solusi, ditemukan di pertanyaan lain oleh Zgoda :

import fcntl
import os
import sys

def instance_already_running(label="default"):
    """
    Detect if an an instance with the label is already running, globally
    at the operating system level.

    Using `os.open` ensures that the file pointer won't be closed
    by Python's garbage collector after the function's scope is exited.

    The lock will be released when the program exits, or could be
    released if the file pointer were closed.
    """

    lock_file_pointer = os.open(f"/tmp/instance_{label}.lock", os.O_WRONLY)

    try:
        fcntl.lockf(lock_file_pointer, fcntl.LOCK_EX | fcntl.LOCK_NB)
        already_running = False
    except IOError:
        already_running = True

    return already_running

Sangat mirip dengan saran S. Lott, tetapi dengan kodenya.


Karena penasaran: apakah ini benar-benar lintas platform? Apakah ini berfungsi di Windows?
Joachim Sauer

1
Tidak ada fcntlmodul di Windows (meskipun fungsinya dapat ditiru).
jfs

10
TIP: jika Anda ingin membungkus ini dalam fungsi 'fp' harus global atau file akan ditutup setelah fungsi keluar.
cmcginty

1
@Mirko Control + Z tidak keluar dari aplikasi (pada OS apa pun yang saya ketahui), ia akan menangguhkannya. Aplikasi dapat dikembalikan ke latar depan dengan fg. Jadi, sepertinya itu berfungsi dengan benar untuk Anda (yaitu, aplikasi masih aktif, tetapi ditangguhkan, jadi kuncinya tetap di tempatnya).
Sam Bull

1
Kode ini dalam situasi saya (Python 3.8.3 di Linux) memerlukan modyfication:lock_file_pointer = os.open(lock_path, os.O_WRONLY | os.O_CREAT)
baziorek

30

Kode ini khusus untuk Linux. Ini menggunakan soket domain UNIX 'abstrak', tetapi sederhana dan tidak akan meninggalkan file kunci basi. Saya lebih suka solusi di atas karena tidak memerlukan port TCP khusus yang dipesan.

try:
    import socket
    s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
    ## Create an abstract socket, by prefixing it with null. 
    s.bind( '\0postconnect_gateway_notify_lock') 
except socket.error as e:
    error_code = e.args[0]
    error_string = e.args[1]
    print "Process already running (%d:%s ). Exiting" % ( error_code, error_string) 
    sys.exit (0) 

String unik postconnect_gateway_notify_lockdapat diubah untuk memungkinkan beberapa program yang membutuhkan satu contoh diberlakukan.


1
Roberto, apakah Anda yakin bahwa setelah kernel panic atau hard reset, file \ 0postconnect_gateway_notify_lock tidak akan muncul saat boot? Dalam kasus saya, file soket AF_UNIX masih ada setelah ini dan ini menghancurkan seluruh ide. Solusi di atas dengan memperoleh kunci pada nama file tertentu jauh lebih dapat diandalkan dalam kasus ini.
Danylo Gurianov

2
Seperti disebutkan di atas, solusi ini berfungsi di Linux tetapi tidak di Mac OS X.
Bilal dan Olga

2
Solusi ini tidak berhasil. Saya mencobanya di Ubuntu 14.04. Jalankan skrip yang sama dari 2 jendela terminal secara bersamaan. Mereka berdua baik-baik saja.
Dimon

1
Ini bekerja untuk saya di Ubuntu 16. Dan mematikan proses dengan cara apa pun memungkinkan yang lain untuk memulai. Dimon Saya pikir Anda melakukan sesuatu yang salah dalam ujian Anda. (Mungkin Anda lupa untuk membuat skrip Anda tidur setelah kode di atas berjalan, sehingga segera keluar dan melepaskan soket.)
Luke

1
Ini bukan soal tidur. Kode berfungsi tetapi hanya sebagai kode sebaris. Saya memasukkannya ke dalam sebuah fungsi. Soket menghilang segera setelah fungsinya ada.
Steve Cohen

25

Saya tidak tahu apakah itu cukup pythonic, tetapi di dunia Java, mendengarkan pada port yang ditentukan adalah solusi yang cukup banyak digunakan, karena berfungsi pada semua platform utama dan tidak memiliki masalah dengan program yang mogok.

Keuntungan lain dari mendengarkan port adalah Anda dapat mengirim perintah ke instance yang sedang berjalan. Misalnya ketika pengguna memulai program untuk kedua kalinya, Anda dapat mengirimkan perintah yang sedang berjalan untuk memintanya membuka jendela lain (itulah yang dilakukan Firefox, misalnya. Saya tidak tahu apakah mereka menggunakan port TCP atau pipa bernama atau sesuatu seperti itu, ').


+1 untuk ini, khususnya karena ini memungkinkan saya untuk memberi tahu instance yang sedang berjalan, sehingga ini membuat jendela lain, muncul, dll.
WhyNotHugo

1
Gunakan mis import socket; s = socket.socket(socket.AF_INET, socket.SOCK_STREAM); s.bind(('localhost', DEFINED_PORT)). Sebuah OSErrorakan dimunculkan jika proses lain terikat ke port yang sama.
crishoj

13

Belum pernah menulis python sebelumnya, tetapi inilah yang baru saja saya terapkan di mycheckpoint, untuk mencegahnya dimulai dua kali atau lebih dengan crond:

import os
import sys
import fcntl
fh=0
def run_once():
    global fh
    fh=open(os.path.realpath(__file__),'r')
    try:
        fcntl.flock(fh,fcntl.LOCK_EX|fcntl.LOCK_NB)
    except:
        os._exit(0)

run_once()

Menemukan saran Slava-N setelah memposting ini di terbitan lain (http://stackoverflow.com/questions/2959474). Yang ini disebut sebagai fungsi, mengunci file skrip yang sedang dieksekusi (bukan file pid) dan mempertahankan kunci sampai skrip berakhir (normal atau error).


1
Sangat elegan. Saya mengubahnya sehingga mendapat jalur dari argumen skrip. Juga merekomendasikan untuk menyematkan ini di tempat yang umum - Contoh
Jossef Harush

10

Gunakan file pid. Anda memiliki beberapa lokasi yang diketahui, "/ path / to / pidfile" dan saat startup Anda melakukan sesuatu seperti ini (sebagian pseudocode karena saya sedang pre-coffee dan tidak ingin bekerja terlalu keras):

import os, os.path
pidfilePath = """/path/to/pidfile"""
if os.path.exists(pidfilePath):
   pidfile = open(pidfilePath,"r")
   pidString = pidfile.read()
   if <pidString is equal to os.getpid()>:
      # something is real weird
      Sys.exit(BADCODE)
   else:
      <use ps or pidof to see if the process with pid pidString is still running>
      if  <process with pid == 'pidString' is still running>:
          Sys.exit(ALREADAYRUNNING)
      else:
          # the previous server must have crashed
          <log server had crashed>
          <reopen pidfilePath for writing>
          pidfile.write(os.getpid())
else:
    <open pidfilePath for writing>
    pidfile.write(os.getpid())

Jadi, dengan kata lain, Anda memeriksa apakah pidfile ada; jika tidak, tulis pid Anda ke file itu. Jika pidfile memang ada, maka periksa untuk melihat apakah pid adalah pid dari proses yang sedang berjalan; jika demikian, maka Anda memiliki proses langsung lainnya yang sedang berjalan, jadi matikan saja. Jika tidak, maka proses sebelumnya macet, jadi catatlah, dan kemudian tulis pid Anda sendiri ke file di tempat yang lama. Kemudian lanjutkan.


4
Ini memiliki kondisi balapan. Urutan test-then-write dapat memunculkan pengecualian dari dua program yang dimulai hampir secara bersamaan, tidak menemukan file dan mencoba membuka untuk menulis secara bersamaan. Ini harus memunculkan pengecualian pada satu, memungkinkan yang lain untuk melanjutkan.
S. Lott


5

Ini mungkin berhasil.

  1. Coba buat file PID ke lokasi yang diketahui. Jika Anda gagal, seseorang telah mengunci file, Anda sudah selesai.

  2. Saat Anda selesai secara normal, tutup dan hapus file PID, sehingga orang lain dapat menimpanya.

Anda dapat membungkus program Anda dalam skrip shell yang menghapus file PID bahkan jika program Anda macet.

Anda juga dapat menggunakan file PID untuk mematikan program jika program tersebut hang.


3

Menggunakan file kunci adalah pendekatan yang cukup umum pada unix. Jika macet, Anda harus membersihkannya secara manual. Anda dapat menyimpan PID dalam file, dan saat startup memeriksa apakah ada proses dengan PID ini, menimpa file kunci jika tidak. (Namun, Anda juga membutuhkan kunci di sekitar read-file-check-pid-rewrite-file). Anda akan menemukan apa yang Anda butuhkan untuk mendapatkan dan memeriksa pid di os -package. Cara umum untuk memeriksa apakah ada proses dengan pid yang diberikan, adalah mengirimkannya sinyal non-fatal.

Alternatif lain dapat menggabungkan ini dengan semaphores flock atau posix.

Membuka soket jaringan, seperti yang diusulkan saua, mungkin akan menjadi yang termudah dan paling portabel.


3

Untuk siapa saja yang menggunakan wxPython untuk aplikasinya, Anda dapat menggunakan fungsi yang wx.SingleInstanceChecker didokumentasikan di sini .

Saya pribadi menggunakan subclass wx.Appyang memanfaatkan wx.SingleInstanceCheckerdan mengembalikan Falsedari OnInit()jika ada instance aplikasi yang sudah ada yang berjalan seperti ini:

import wx

class SingleApp(wx.App):
    """
    class that extends wx.App and only permits a single running instance.
    """

    def OnInit(self):
        """
        wx.App init function that returns False if the app is already running.
        """
        self.name = "SingleApp-%s".format(wx.GetUserId())
        self.instance = wx.SingleInstanceChecker(self.name)
        if self.instance.IsAnotherRunning():
            wx.MessageBox(
                "An instance of the application is already running", 
                "Error", 
                 wx.OK | wx.ICON_WARNING
            )
            return False
        return True

Ini adalah pengganti sederhana untuk wx.Appyang melarang beberapa contoh. Untuk menggunakannya cukup ganti wx.Appdengan SingleAppdalam kode Anda seperti ini:

app = SingleApp(redirect=False)
frame = wx.Frame(None, wx.ID_ANY, "Hello World")
frame.Show(True)
app.MainLoop()

Setelah mengkodekan utas daftar soket untuk singleton, saya menemukan ini, yang berfungsi dengan baik dan saya sudah menginstal di program pasangan, namun, saya ingin tambahan "bangun" yang bisa saya berikan pada singleton sehingga saya dapat membawanya ke depan-dan-tengah tumpukan besar jendela yang tumpang tindih. Juga: tautan "didokumentasikan di sini" mengarah ke dokumentasi yang dibuat secara otomatis dan tidak berguna. Ini adalah tautan yang lebih baik
RufusVS

@RufusVS Anda benar - itu tautan dokumentasi yang jauh lebih baik, telah memperbarui jawabannya.
Matt Coubrough

3

Berikut adalah solusi khusus Windows saya. Letakkan berikut ini ke dalam modul, mungkin disebut 'onlyone.py', atau apa pun. Sertakan modul itu langsung ke file skrip python __ main __ Anda.

import win32event, win32api, winerror, time, sys, os
main_path = os.path.abspath(sys.modules['__main__'].__file__).replace("\\", "/")

first = True
while True:
        mutex = win32event.CreateMutex(None, False, main_path + "_{<paste YOUR GUID HERE>}")
        if win32api.GetLastError() == 0:
            break
        win32api.CloseHandle(mutex)
        if first:
            print "Another instance of %s running, please wait for completion" % main_path
            first = False
        time.sleep(1)

Penjelasan

Kode mencoba membuat mutex dengan nama yang diturunkan dari jalur lengkap ke skrip. Kami menggunakan garis miring ke depan untuk menghindari potensi kebingungan dengan sistem file yang sebenarnya.

Keuntungan

  • Tidak ada konfigurasi atau pengenal 'ajaib' yang diperlukan, gunakan itu dalam banyak skrip berbeda sesuai kebutuhan.
  • Tidak ada file basi yang tersisa, mutex mati bersama Anda.
  • Mencetak pesan berguna saat menunggu

3

Solusi terbaik untuk ini di windows adalah dengan menggunakan mutex seperti yang disarankan oleh @zgoda.

import win32event
import win32api
from winerror import ERROR_ALREADY_EXISTS

mutex = win32event.CreateMutex(None, False, 'name')
last_error = win32api.GetLastError()

if last_error == ERROR_ALREADY_EXISTS:
   print("App instance already running")

Beberapa jawaban menggunakan fctnl(termasuk juga dalam paket @sorin tendo) yang tidak tersedia di windows dan jika Anda mencoba untuk membekukan aplikasi python Anda menggunakan paket seperti pyinstalleryang melakukan impor statis, itu membuat kesalahan.

Selain itu, menggunakan metode file kunci, menciptakan read-onlymasalah dengan file database (mengalami ini dengan sqlite3).


2

Saya memposting ini sebagai jawaban karena saya adalah pengguna baru dan Stack Overflow tidak mengizinkan saya memilih.

Solusi Sorin Sbarnea bekerja untuk saya di bawah OS X, Linux dan Windows, dan saya berterima kasih untuk itu.

Namun, tempfile.gettempdir () berperilaku satu cara di bawah OS X dan Windows dan yang lain di bawah beberapa / banyak / semua (?) * Nixes (mengabaikan fakta bahwa OS X juga Unix!). Perbedaannya penting untuk kode ini.

OS X dan Windows memiliki direktori temporer khusus pengguna, jadi file temporer yang dibuat oleh satu pengguna tidak terlihat oleh pengguna lain. Sebaliknya, di bawah banyak versi * nix (saya menguji Ubuntu 9, RHEL 5, OpenSolaris 2008 dan FreeBSD 8), temp dirnya adalah / tmp untuk semua pengguna.

Artinya, saat lockfile dibuat di mesin multi-pengguna, lockfile dibuat di / tmp dan hanya pengguna yang membuat lockfile pertama kali yang dapat menjalankan aplikasi.

Solusi yang mungkin adalah menyematkan nama pengguna saat ini di nama file kunci.

Perlu dicatat bahwa solusi OP untuk mengambil port juga akan berperilaku buruk pada mesin multi-pengguna.


Untuk beberapa pembaca (misalnya saya) perilaku yang diinginkan adalah bahwa hanya satu salinan yang dapat menjalankan periode, terlepas dari berapa banyak pengguna yang terlibat. Jadi direktori tmp per pengguna rusak, sedangkan shared / tmp atau kunci port menunjukkan perilaku yang diinginkan.
Jonathan Hartley


1

Saya terus curiga seharusnya ada solusi POSIXy yang baik menggunakan grup proses, tanpa harus menekan sistem file, tetapi saya tidak bisa memahaminya. Sesuatu seperti:

Saat memulai, proses Anda mengirimkan 'kill -0' ke semua proses dalam grup tertentu. Jika ada proses seperti itu, itu keluar. Kemudian ia bergabung dengan grup. Tidak ada proses lain yang menggunakan grup itu.

Namun, ini memiliki syarat perlombaan - beberapa proses dapat melakukannya pada waktu yang sama dan semuanya berakhir dengan bergabung ke grup dan berjalan secara bersamaan. Pada saat Anda telah menambahkan semacam mutex untuk membuatnya kedap air, Anda tidak lagi memerlukan grup proses.

Ini mungkin dapat diterima jika proses Anda hanya dimulai dengan cron, sekali setiap menit atau setiap jam, tetapi itu membuat saya sedikit gugup karena akan salah tepat pada hari ketika Anda tidak menginginkannya.

Saya kira ini bukan solusi yang sangat baik, kecuali seseorang dapat memperbaikinya?


1

Saya mengalami masalah ini minggu lalu, dan meskipun saya menemukan beberapa solusi yang baik, saya memutuskan untuk membuat paket python yang sangat sederhana dan bersih dan mengunggahnya ke PyPI. Ini berbeda dari tendo karena dapat mengunci nama sumber daya string apa pun. Meskipun Anda pasti bisa mengunci __file__untuk mendapatkan efek yang sama.

Instal dengan: pip install quicklock

Menggunakannya sangat sederhana:

[nate@Nates-MacBook-Pro-3 ~/live] python
Python 2.7.6 (default, Sep  9 2014, 15:04:36)
[GCC 4.2.1 Compatible Apple LLVM 6.0 (clang-600.0.39)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from quicklock import singleton
>>> # Let's create a lock so that only one instance of a script will run
...
>>> singleton('hello world')
>>>
>>> # Let's try to do that again, this should fail
...
>>> singleton('hello world')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/nate/live/gallery/env/lib/python2.7/site-packages/quicklock/quicklock.py", line 47, in singleton
    raise RuntimeError('Resource <{}> is currently locked by <Process {}: "{}">'.format(resource, other_process.pid, other_process.name()))
RuntimeError: Resource <hello world> is currently locked by <Process 24801: "python">
>>>
>>> # But if we quit this process, we release the lock automatically
...
>>> ^D
[nate@Nates-MacBook-Pro-3 ~/live] python
Python 2.7.6 (default, Sep  9 2014, 15:04:36)
[GCC 4.2.1 Compatible Apple LLVM 6.0 (clang-600.0.39)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from quicklock import singleton
>>> singleton('hello world')
>>>
>>> # No exception was thrown, we own 'hello world'!

Lihat: https://pypi.python.org/pypi/quicklock


1
Saya baru saja menginstalnya melalui "pip install quicklock", tetapi ketika saya mencoba menggunakannya melalui "dari quicklock import singleton" saya mendapatkan pengecualian: "ImportError: tidak dapat mengimpor nama 'tunggal'". Ini di Mac.
grayaii

Ternyata quicklock tidak berfungsi dengan python 3. Itulah alasan mengapa saya gagal.
grayaii

Ya, maaf, itu sama sekali tidak memiliki bukti masa depan. Saya akan menyambut baik kontribusi untuk membuatnya bekerja!
Nate Ferrero

1

Berdasarkan jawaban Roberto Rosario, saya menemukan fungsi berikut:

SOCKET = None
def run_single_instance(uniq_name):
    try:
        import socket
        global SOCKET
        SOCKET = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
        ## Create an abstract socket, by prefixing it with null.
        # this relies on a feature only in linux, when current process quits, the
        # socket will be deleted.
        SOCKET.bind('\0' + uniq_name)
        return True
    except socket.error as e:
        return False

Kita perlu mendefinisikan global SOCKETvaiable karena ini hanya akan menjadi sampah yang dikumpulkan ketika seluruh proses berhenti. Jika kita mendeklarasikan variabel lokal dalam fungsinya, itu akan keluar dari ruang lingkup setelah fungsi keluar, sehingga soket dihapus.

Semua pujian harus diberikan kepada Roberto Rosario, karena saya hanya mengklarifikasi dan menguraikan kodenya. Dan kode ini hanya akan berfungsi di Linux, seperti yang dijelaskan teks yang dikutip berikut dari https://troydhanson.github.io/network/Unix_domain_sockets.html :

Linux memiliki fitur khusus: jika nama jalur untuk soket domain UNIX dimulai dengan byte nol \ 0, namanya tidak dipetakan ke dalam sistem berkas. Jadi itu tidak akan bertabrakan dengan nama lain di sistem file. Selain itu, saat server menutup soket mendengarkan domain UNIX di namespace abstrak, file tersebut dihapus; dengan soket domain UNIX biasa, file tetap ada setelah server menutupnya.


0

contoh linux

Metode ini didasarkan pada pembuatan file sementara yang otomatis dihapus setelah Anda menutup aplikasi. peluncuran program kami memverifikasi keberadaan file; jika file ada (ada eksekusi yang tertunda), program ditutup; jika tidak, ia membuat file dan melanjutkan eksekusi program.

from tempfile import *
import time
import os
import sys


f = NamedTemporaryFile( prefix='lock01_', delete=True) if not [f  for f in     os.listdir('/tmp') if f.find('lock01_')!=-1] else sys.exit()

YOUR CODE COMES HERE

1
Selamat datang di Stack Overflow! Meskipun jawaban ini mungkin benar, harap tambahkan beberapa penjelasan. Menanamkan logika yang mendasarinya lebih penting daripada hanya memberikan kode, karena ini membantu OP dan pembaca lain memperbaiki sendiri masalah ini dan masalah serupa.
CodeMouse92

Apakah thread ini aman? Sepertinya pemeriksaan dan pembuatan file temp tidak atom ...
coppit

0

Pada sistem Linux, seseorang juga dapat menanyakan pgrep -ajumlah instance, skrip ditemukan dalam daftar proses (opsi -a mengungkapkan string baris perintah lengkap). Misalnya

import os
import sys
import subprocess

procOut = subprocess.check_output( "/bin/pgrep -u $UID -a python", shell=True, 
                                   executable="/bin/bash", universal_newlines=True)

if procOut.count( os.path.basename(__file__)) > 1 :        
    sys.exit( ("found another instance of >{}<, quitting."
              ).format( os.path.basename(__file__)))

Hapus -u $UIDjika batasan berlaku untuk semua pengguna. Penafian: a) diasumsikan bahwa nama skrip (basis) unik, b) mungkin ada kondisi balapan.


-1
import sys,os

# start program
try:  # (1)
    os.unlink('lock')  # (2)
    fd=os.open("lock", os.O_CREAT|os.O_EXCL) # (3)  
except: 
    try: fd=os.open("lock", os.O_CREAT|os.O_EXCL) # (4) 
    except:  
        print "Another Program running !.."  # (5)
        sys.exit()  

# your program  ...
# ...

# exit program
try: os.close(fd)  # (6)
except: pass
try: os.unlink('lock')  
except: pass
sys.exit()  

2
Selamat datang di Stack Overflow! Meskipun blok kode ini dapat menjawab pertanyaan tersebut, akan lebih baik jika Anda dapat memberikan sedikit penjelasan mengapa hal itu terjadi. Harap edit jawaban Anda untuk memasukkan deskripsi seperti itu.
Artjom B.
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.