LinuxMonitor 1.1.1__tar.gz → 1.1.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: LinuxMonitor
3
- Version: 1.1.1
3
+ Version: 1.1.2
4
4
  Summary: Get information and warning status of Linux server like service, port, ping, ssl certificate, disk/folder/cpu/ram/swap usage, ip connection, ... (Python and shell library, Linux ONLY)
5
5
  Home-page: https://github.com/QuentinCG/Linux-Monitor-Python-Library
6
6
  Author: Quentin Comte-Gaz
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: LinuxMonitor
3
- Version: 1.1.1
3
+ Version: 1.1.2
4
4
  Summary: Get information and warning status of Linux server like service, port, ping, ssl certificate, disk/folder/cpu/ram/swap usage, ip connection, ... (Python and shell library, Linux ONLY)
5
5
  Home-page: https://github.com/QuentinCG/Linux-Monitor-Python-Library
6
6
  Author: Quentin Comte-Gaz
@@ -98,7 +98,7 @@ def main() -> None:
98
98
  if args.ping:
99
99
  handled = True
100
100
  print("Pinging websites...")
101
- out_msg: str = monitoring.ping_all_websites(is_private=True, display_only_if_critical=False)
101
+ out_msg: str = asyncio.run(monitoring.ping_all_websites(is_private=True, display_only_if_critical=False))
102
102
  print(out_msg)
103
103
 
104
104
  if args.certificates:
@@ -110,24 +110,24 @@ def main() -> None:
110
110
  if args.reboot_server:
111
111
  handled = True
112
112
  print("Restarting the entire server...")
113
- out_msg: str = monitoring.reboot_server()
113
+ out_msg: str = asyncio.run(monitoring.reboot_server())
114
114
  print(out_msg)
115
115
 
116
116
  if args.services_status:
117
117
  handled = True
118
118
  print("Checking if services are running and restart if down...")
119
- out_msg: str = monitoring.check_all_services_status_and_restart_if_down(is_private=True)
119
+ out_msg: str = asyncio.run(monitoring.check_all_services_status_and_restart_if_down(is_private=True))
120
120
  print(out_msg)
121
121
 
122
122
  if args.restart_all:
123
123
  handled = True
124
124
  print("Restarting all services...")
125
- out_msg: str = monitoring.restart_all_services(is_private=True)
125
+ out_msg: str = asyncio.run(monitoring.restart_all_services(is_private=True))
126
126
 
127
127
  if args.restart_service is not None:
128
128
  handled = True
129
129
  print(f"Restarting service: {args.restart_service}...")
130
- out_msg: str = monitoring.restart_service(is_private=True, service_name=args.restart_service)
130
+ out_msg: str = asyncio.run(monitoring.restart_service(is_private=True, service_name=args.restart_service))
131
131
  print(out_msg)
132
132
 
133
133
  if args.list_services:
@@ -139,7 +139,7 @@ def main() -> None:
139
139
  if args.ports:
140
140
  handled = True
141
141
  print("Checking ports...")
142
- out_msg: str = monitoring.check_all_ports(is_private=True, display_only_if_critical=False, restart_if_down=False)
142
+ out_msg: str = asyncio.run(monitoring.check_all_ports(is_private=True, display_only_if_critical=False, restart_if_down=False))
143
143
  print(out_msg)
144
144
 
145
145
  if args.list_processes:
@@ -151,7 +151,7 @@ def main() -> None:
151
151
  if args.kill_process is not None:
152
152
  handled = True
153
153
  print(f"Stopping process with PID: {args.kill_process}...")
154
- out_msg: str = monitoring.kill_process(pid=args.kill_process)
154
+ out_msg: str = asyncio.run(monitoring.kill_process(pid=args.kill_process))
155
155
  print(out_msg)
156
156
 
157
157
  if args.start_scheduled_task_check_for_issues:
@@ -29,7 +29,7 @@ __email__ = "quentin@comte-gaz.com"
29
29
  __license__ = "MIT License"
30
30
  __copyright__ = "Copyright Quentin Comte-Gaz (2024)"
31
31
  __python_version__ = "3.+"
32
- __version__ = "1.1.1 (2024/09/14)"
32
+ __version__ = "1.1.2 (2024/09/14)"
33
33
  __status__ = "Usable for any Linux project"
34
34
 
35
35
  import json
@@ -174,9 +174,9 @@ class LinuxMonitor:
174
174
 
175
175
  #region Execute Command
176
176
 
177
- def execute_and_verify(self, command: List[str], display_name: str, timeout_in_sec: Optional[int] = None, display_only_if_critical: bool = False, check_also_stdout_not_containing: Optional[str] = None) -> Tuple[Optional[bool], str]:
177
+ async def execute_and_verify(self, command: List[str], display_name: str, timeout_in_sec: Optional[int] = None, display_only_if_critical: bool = False, check_also_stdout_not_containing: Optional[str] = None) -> Tuple[Optional[bool], str]:
178
178
  """
179
- Execute a shell command and verify its correct execution.
179
+ Execute a shell command asynchronously and verify its correct execution.
180
180
 
181
181
  :param command: A list of strings representing the command and its arguments.
182
182
  :param display_name: The name of the command to display in the output message.
@@ -186,18 +186,24 @@ class LinuxMonitor:
186
186
 
187
187
  :return: A tuple containing a boolean indicating if the command was executed successfully and a string containing the result message.
188
188
  """
189
- returncode: int = -1
189
+ returncode: Optional[int] = None
190
190
 
191
191
  try:
192
192
  logging.debug(msg=f"Executing command {display_name} (command: {command})...")
193
- pipe = subprocess.Popen(args=command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False)
193
+ process = await asyncio.create_subprocess_exec(
194
+ *command,
195
+ stdout=asyncio.subprocess.PIPE,
196
+ stderr=asyncio.subprocess.PIPE,
197
+ shell=False
198
+ )
199
+
194
200
  try:
195
- stdout, stderr = pipe.communicate(timeout=timeout_in_sec)
196
- returncode = pipe.returncode
197
- except subprocess.TimeoutExpired:
201
+ stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=timeout_in_sec)
202
+ returncode = process.returncode
203
+ except asyncio.TimeoutError:
198
204
  logging.warning(msg=f"Timeout expired for {display_name} (command: {command})")
199
- pipe.kill()
200
- stdout, stderr = pipe.communicate()
205
+ process.kill()
206
+ return None, f"⚠️ **Error {display_name}**:\n- Failed to execute the command in less than {timeout_in_sec} seconds)"
201
207
  except Exception as e:
202
208
  # Failed to execute the command
203
209
  out_msg = f"⚠️ **Error {display_name}**:\n```sh\n{e}\n```"
@@ -209,7 +215,7 @@ class LinuxMonitor:
209
215
  stderr_str: str = stderr.decode(encoding='utf-8').replace('`', '"').strip()
210
216
 
211
217
  # Check if the command executed successfully
212
- res: Optional[bool] = (returncode == 0) if (returncode != -1) else None
218
+ res: Optional[bool] = (returncode == 0) if (returncode != None) else None
213
219
  if res and check_also_stdout_not_containing:
214
220
  res = check_also_stdout_not_containing not in stdout_str
215
221
 
@@ -244,7 +250,7 @@ class LinuxMonitor:
244
250
 
245
251
  #region Reboot
246
252
 
247
- def reboot_server(self) -> str:
253
+ async def reboot_server(self) -> str:
248
254
  """
249
255
  Reboot the server.
250
256
 
@@ -253,7 +259,7 @@ class LinuxMonitor:
253
259
  IMPORTANT: To be usable, the user must have the right to execute `sudo /sbin/reboot` command without password.
254
260
  """
255
261
  logging.info(msg="Rebooting the server...")
256
- _, out_msg = self.execute_and_verify(command=["sudo", "/sbin/reboot"], display_name="Server reboot", timeout_in_sec=None, display_only_if_critical=False)
262
+ _, out_msg = await self.execute_and_verify(command=["sudo", "/sbin/reboot"], display_name="Server reboot", timeout_in_sec=None, display_only_if_critical=False)
257
263
  return out_msg
258
264
 
259
265
  #endregion
@@ -721,7 +727,7 @@ class LinuxMonitor:
721
727
 
722
728
  #region Ping Websites
723
729
 
724
- def _ping_website(self, website: str, display_name: str, timeout_in_sec: int, display_only_if_critical: bool=False) -> str:
730
+ async def _ping_website(self, website: str, display_name: str, timeout_in_sec: int, display_only_if_critical: bool=False) -> str:
725
731
  """
726
732
  Ping a website.
727
733
 
@@ -735,7 +741,7 @@ class LinuxMonitor:
735
741
  display_name = f"[{display_name}](https://{website})"
736
742
 
737
743
  start_time: float = time.time()
738
- res, out_msg = self.execute_and_verify(command=ping_command, display_name=f"ping {display_name}", timeout_in_sec=timeout_in_sec, display_only_if_critical=display_only_if_critical)
744
+ res, out_msg = await self.execute_and_verify(command=ping_command, display_name=f"ping {display_name}", timeout_in_sec=timeout_in_sec, display_only_if_critical=display_only_if_critical)
739
745
  end_time: float = time.time()
740
746
 
741
747
  if res == True and not display_only_if_critical:
@@ -745,7 +751,7 @@ class LinuxMonitor:
745
751
 
746
752
  return out_msg
747
753
 
748
- def ping_all_websites(self, is_private: bool, display_only_if_critical: bool=False) -> str:
754
+ async def ping_all_websites(self, is_private: bool, display_only_if_critical: bool=False) -> str:
749
755
  """
750
756
  Ping all websites configured in the JSON configuration file.
751
757
 
@@ -758,7 +764,7 @@ class LinuxMonitor:
758
764
  for ping_config in self.config['pings']:
759
765
  if is_private or is_private == ping_config['is_private']:
760
766
  timeout_in_sec: int = ping_config.get('timeout_in_sec', 5)
761
- result: str = self._ping_website(website=ping_config['website'], display_name=ping_config['display_name'], timeout_in_sec=timeout_in_sec, display_only_if_critical=display_only_if_critical)
767
+ result: str = await self._ping_website(website=ping_config['website'], display_name=ping_config['display_name'], timeout_in_sec=timeout_in_sec, display_only_if_critical=display_only_if_critical)
762
768
  if result:
763
769
  if out_msg:
764
770
  out_msg += "\n"
@@ -801,7 +807,7 @@ class LinuxMonitor:
801
807
  logging.info(msg=out_msg)
802
808
  return out_msg
803
809
 
804
- def restart_service(self, is_private: bool, service_name: str) -> str:
810
+ async def restart_service(self, is_private: bool, service_name: str) -> str:
805
811
  """
806
812
  Restart a specific service.
807
813
 
@@ -835,7 +841,7 @@ class LinuxMonitor:
835
841
  logging.info(f"Trying to restart {display_name} (command: {service_call}) in less than {timeout_in_sec}sec...")
836
842
 
837
843
  start_time: float = time.time()
838
- res, out_msg = self.execute_and_verify(command=service_call, display_name=f"restart {display_name}", timeout_in_sec=timeout_in_sec, display_only_if_critical=False)
844
+ res, out_msg = await self.execute_and_verify(command=service_call, display_name=f"restart {display_name}", timeout_in_sec=timeout_in_sec, display_only_if_critical=False)
839
845
  end_time: float = time.time()
840
846
 
841
847
  if res == True:
@@ -845,7 +851,7 @@ class LinuxMonitor:
845
851
 
846
852
  return out_msg
847
853
 
848
- def restart_all_services(self, is_private: bool) -> str:
854
+ async def restart_all_services(self, is_private: bool) -> str:
849
855
  """
850
856
  Restart all services allowed to restart which are configured in the JSON configuration file.
851
857
 
@@ -856,7 +862,7 @@ class LinuxMonitor:
856
862
  out_msg: str = ""
857
863
  for service_name in self.config["services"].keys():
858
864
  if is_private or self.config["services"][service_name]['is_private'] == is_private:
859
- res: str = self.restart_service(is_private=is_private, service_name=service_name)
865
+ res: str = await self.restart_service(is_private=is_private, service_name=service_name)
860
866
  if res != "":
861
867
  if out_msg:
862
868
  out_msg += "\n"
@@ -867,7 +873,7 @@ class LinuxMonitor:
867
873
 
868
874
  return out_msg
869
875
 
870
- def _get_service_status(self, is_private: bool, service_name: str) -> Tuple[Optional[bool], str]:
876
+ async def _get_service_status(self, is_private: bool, service_name: str) -> Tuple[Optional[bool], str]:
871
877
  """
872
878
  Get the status of a specific service.
873
879
 
@@ -891,10 +897,10 @@ class LinuxMonitor:
891
897
  status_command = service['status_command']
892
898
  check_also_stdout_not_containing = service.get('check_also_stdout_not_containing', None)
893
899
 
894
- res, out_msg = self.execute_and_verify(command=status_command, display_name=f"état de {display_name}", timeout_in_sec=timeout_in_sec, display_only_if_critical=False, check_also_stdout_not_containing=check_also_stdout_not_containing)
900
+ res, out_msg = await self.execute_and_verify(command=status_command, display_name=f"état de {display_name}", timeout_in_sec=timeout_in_sec, display_only_if_critical=False, check_also_stdout_not_containing=check_also_stdout_not_containing)
895
901
  return res, out_msg
896
902
 
897
- def check_all_services_status(self, is_private: bool) -> str:
903
+ async def check_all_services_status(self, is_private: bool) -> str:
898
904
  """
899
905
  Check the status of all services configured in the JSON configuration file.
900
906
 
@@ -923,7 +929,7 @@ class LinuxMonitor:
923
929
 
924
930
  for service_name in self.config["services"].keys():
925
931
  if is_private or self.config["services"][service_name]['is_private'] == is_private:
926
- status, status_msg = self._get_service_status(is_private=is_private, service_name=service_name)
932
+ status, status_msg = await self._get_service_status(is_private=is_private, service_name=service_name)
927
933
  service_status: str = status_to_string(res=status)
928
934
  service_icon: str = status_to_icon(res=status)
929
935
  if out_msg != "":
@@ -937,7 +943,7 @@ class LinuxMonitor:
937
943
 
938
944
  return out_msg
939
945
 
940
- def check_all_services_status_and_restart_if_down(self, is_private: bool) -> str:
946
+ async def check_all_services_status_and_restart_if_down(self, is_private: bool) -> str:
941
947
  """
942
948
  Check the status of all services configured in the JSON configuration file and restart them if they are inactive.
943
949
 
@@ -949,7 +955,7 @@ class LinuxMonitor:
949
955
  try:
950
956
  for service_name in self.config["services"].keys():
951
957
  if is_private or self.config["services"][service_name]['is_private'] == is_private:
952
- status, status_msg = self._get_service_status(is_private=is_private, service_name=service_name)
958
+ status, status_msg = await self._get_service_status(is_private=is_private, service_name=service_name)
953
959
  if status is False:
954
960
  out_msg: str = f"❌ **{self.config['services'][service_name]['display_name']} inactive**. Restarting the service is necessary."
955
961
  logging.warning(msg=out_msg)
@@ -957,7 +963,7 @@ class LinuxMonitor:
957
963
  out_msg += f"\n{status_msg}"
958
964
 
959
965
  out_msg_full += out_msg + "\n"
960
- out_msg_full += self.restart_service(is_private=is_private, service_name=service_name) + "\n"
966
+ out_msg_full += await self.restart_service(is_private=is_private, service_name=service_name) + "\n"
961
967
  elif status is None:
962
968
  out_msg: str = f"⚠️ **Error checking status of {self.config['services'][service_name]['display_name']}** (not restarting it)."
963
969
  logging.error(msg=out_msg)
@@ -1371,7 +1377,7 @@ class LinuxMonitor:
1371
1377
 
1372
1378
  return res, out_msg
1373
1379
 
1374
- def check_all_ports(self, is_private: bool, display_only_if_critical: bool=False, restart_if_down: bool=False) -> str:
1380
+ async def check_all_ports(self, is_private: bool, display_only_if_critical: bool=False, restart_if_down: bool=False) -> str:
1375
1381
  """
1376
1382
  Check all ports configured in the JSON configuration file (and restart the service if the port is down and service_name_to_restart added in config file).
1377
1383
 
@@ -1405,7 +1411,7 @@ class LinuxMonitor:
1405
1411
  if out_msg != "":
1406
1412
  out_msg += "\n"
1407
1413
 
1408
- restart_res: str = self.restart_service(is_private=is_private, service_name=service_name_to_restart)
1414
+ restart_res: str = await self.restart_service(is_private=is_private, service_name=service_name_to_restart)
1409
1415
  out_msg += f" - {restart_res}"
1410
1416
 
1411
1417
  if out_msg != "":
@@ -1484,7 +1490,7 @@ class LinuxMonitor:
1484
1490
  logging.info(msg=full_res)
1485
1491
  return full_res
1486
1492
 
1487
- def kill_process(self, pid: int, timeout_in_sec: int = 10) -> str:
1493
+ async def kill_process(self, pid: int, timeout_in_sec: int = 10) -> str:
1488
1494
  """
1489
1495
  Kills a process with the specified PID.
1490
1496
  1. Tries to terminate the process gracefully
@@ -1522,7 +1528,7 @@ class LinuxMonitor:
1522
1528
 
1523
1529
  # Attempt to terminate the process
1524
1530
  terminate_command: List[str] = ['sudo', '/bin/kill', '-TERM', str(pid)]
1525
- result_terminate, _ = self.execute_and_verify(command=terminate_command, display_name=f"stop process {pid} ({process_name})", timeout_in_sec=timeout_in_sec, display_only_if_critical=False)
1531
+ result_terminate, _ = await self.execute_and_verify(command=terminate_command, display_name=f"stop process {pid} ({process_name})", timeout_in_sec=timeout_in_sec, display_only_if_critical=False)
1526
1532
 
1527
1533
  if result_terminate == True:
1528
1534
  out_msg = f"✅ **Process {pid} ({process_name}) stopped with success** (nicely)).\n"
@@ -1534,7 +1540,7 @@ class LinuxMonitor:
1534
1540
 
1535
1541
  # Attempt to kill the process
1536
1542
  kill_command = ['sudo', '/bin/kill', '-KILL', str(pid)]
1537
- _, strerror_kill = self.execute_and_verify(command=kill_command, display_name=f"kill process {pid} ({process_name})", timeout_in_sec=timeout_in_sec, display_only_if_critical=False)
1543
+ _, strerror_kill = await self.execute_and_verify(command=kill_command, display_name=f"kill process {pid} ({process_name})", timeout_in_sec=timeout_in_sec, display_only_if_critical=False)
1538
1544
  out_msg += strerror_kill
1539
1545
 
1540
1546
  if process_cpu_percent > 0:
@@ -1624,7 +1630,8 @@ class LinuxMonitor:
1624
1630
  if not self.allow_scheduled_tasks_check_for_issues:
1625
1631
  raise Exception("Scheduled tasks are not allowed")
1626
1632
 
1627
- await asyncio.sleep(delay=10) # Sleep for 10 sec before lauching the scheduled tasks (to allow the lib to be ready)
1633
+ logging.info(msg=f"Waiting for 45 seconds before starting the execution of {'private' if is_private else 'public'} scheduled tasks to allow the discord bot and linux server to be ready...")
1634
+ await asyncio.sleep(delay=45) # Sleep for 45 sec before lauching the scheduled tasks (to allow the lib to be ready)
1628
1635
 
1629
1636
  datetime_last_disk_usage_error_displayed: Optional[datetime] = None
1630
1637
  datetime_last_folder_usage_error_displayed: Optional[datetime] = None
@@ -1649,7 +1656,7 @@ class LinuxMonitor:
1649
1656
  logging.info(msg="Checking services status and all disk usage, CPU, RAM, Swap, CPU temperature and ping of websites periodically...")
1650
1657
  try:
1651
1658
  # Services status
1652
- msg: str = self.check_all_services_status_and_restart_if_down(is_private=is_private)
1659
+ msg: str = await self.check_all_services_status_and_restart_if_down(is_private=is_private)
1653
1660
  if msg != "":
1654
1661
  logging.warning(msg=msg)
1655
1662
  if datetime_last_services_error_displayed is None or ((datetime.now() - datetime_last_services_error_displayed).total_seconds() > self.max_duration_seconds_showing_same_error_again_in_scheduled_tasks):
@@ -1733,7 +1740,7 @@ class LinuxMonitor:
1733
1740
  logging.info(msg="- ✅ Certificates are OK.")
1734
1741
 
1735
1742
  # Ping
1736
- msg = self.ping_all_websites(is_private=is_private, display_only_if_critical=True)
1743
+ msg = await self.ping_all_websites(is_private=is_private, display_only_if_critical=True)
1737
1744
  if msg != "":
1738
1745
  logging.warning(msg=msg)
1739
1746
  if datetime_last_ping_error_displayed is None or ((datetime.now() - datetime_last_ping_error_displayed).total_seconds() > self.max_duration_seconds_showing_same_error_again_in_scheduled_tasks):
@@ -1754,7 +1761,7 @@ class LinuxMonitor:
1754
1761
  logging.info(msg="- ✅ Ping of all websites are OK.")
1755
1762
 
1756
1763
  # Ports
1757
- msg = self.check_all_ports(is_private=is_private, display_only_if_critical=True, restart_if_down=True)
1764
+ msg = await self.check_all_ports(is_private=is_private, display_only_if_critical=True, restart_if_down=True)
1758
1765
  if msg != "":
1759
1766
  logging.warning(msg=msg)
1760
1767
  if datetime_last_port_error_displayed is None or ((datetime.now() - datetime_last_port_error_displayed).total_seconds() > self.max_duration_seconds_showing_same_error_again_in_scheduled_tasks):
@@ -1922,7 +1929,8 @@ class LinuxMonitor:
1922
1929
  if not self.allow_scheduled_task_show_info:
1923
1930
  raise Exception("Scheduled show info tasks are not allowed")
1924
1931
 
1925
- await asyncio.sleep(delay=10)
1932
+ logging.info(msg=f"Waiting for 45 seconds before starting the execution of {'private' if is_private else 'public'} show info scheduled tasks to allow the discord bot and linux server to be ready...")
1933
+ await asyncio.sleep(delay=45)
1926
1934
 
1927
1935
  if not self.start_scheduled_task_show_info_immediately:
1928
1936
  logging.info(msg=f"Waiting for {self.duration_in_sec_wait_between_each_schedule_task_show_info_execution} seconds before starting the execution of {'private' if is_private else 'public'} show info scheduled tasks...")
@@ -1934,7 +1942,7 @@ class LinuxMonitor:
1934
1942
  logging.info(msg="-----------------------------------------------")
1935
1943
 
1936
1944
  # Services status
1937
- msg: str = self.check_all_services_status_and_restart_if_down(is_private=is_private)
1945
+ msg: str = await self.check_all_services_status_and_restart_if_down(is_private=is_private)
1938
1946
  if msg != "":
1939
1947
  if out_msg != "":
1940
1948
  out_msg += "\n"
@@ -1962,14 +1970,14 @@ class LinuxMonitor:
1962
1970
  out_msg += msg
1963
1971
 
1964
1972
  # Ping
1965
- msg = self.ping_all_websites(is_private=is_private, display_only_if_critical=False)
1973
+ msg = await self.ping_all_websites(is_private=is_private, display_only_if_critical=False)
1966
1974
  if msg != "":
1967
1975
  if out_msg != "":
1968
1976
  out_msg += "\n"
1969
1977
  out_msg += msg
1970
1978
 
1971
1979
  # Ports
1972
- msg = self.check_all_ports(is_private=is_private, display_only_if_critical=False, restart_if_down=False)
1980
+ msg = await self.check_all_ports(is_private=is_private, display_only_if_critical=False, restart_if_down=False)
1973
1981
  if msg != "":
1974
1982
  if out_msg != "":
1975
1983
  out_msg += "\n"
@@ -6,7 +6,7 @@ with io.open(file='README.md', mode='r', encoding='utf-8') as readme_file:
6
6
 
7
7
  setup(
8
8
  name="LinuxMonitor",
9
- version="1.1.1",
9
+ version="1.1.2",
10
10
  description="Get information and warning status of Linux server like service, port, ping, ssl certificate, disk/folder/cpu/ram/swap usage, ip connection, ... (Python and shell library, Linux ONLY)",
11
11
  long_description=readme,
12
12
  long_description_content_type="text/markdown",
File without changes
File without changes
File without changes