Isolate 작업 추가
This commit is contained in:
parent
e28efbe402
commit
a8a52fb86a
16 changed files with 564 additions and 7 deletions
13
.vscode/launch.json
vendored
Normal file
13
.vscode/launch.json
vendored
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "dart_framework",
|
||||
"request": "launch",
|
||||
"type": "dart"
|
||||
}
|
||||
]
|
||||
}
|
||||
34
bin/main.dart
Normal file
34
bin/main.dart
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import 'package:dart_framework/core/isolate_manager.dart';
|
||||
import 'package:dart_framework/core/log.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
log('test');
|
||||
log('test2');
|
||||
|
||||
test();
|
||||
}
|
||||
|
||||
void test() async {
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
log('this is test1');
|
||||
log('this is test2');
|
||||
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
log('this is test3');
|
||||
log('this is test4');
|
||||
log('this is test5');
|
||||
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
log('this is test6');
|
||||
log('this is test7');
|
||||
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
log('this is test8');
|
||||
log('this is test9');
|
||||
|
||||
await Future.delayed(const Duration(seconds: 3));
|
||||
log('this is test10');
|
||||
log('this is test11');
|
||||
log('this is test12');
|
||||
log('this is test13');
|
||||
}
|
||||
3
json_01.json
Normal file
3
json_01.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"a": "foo"
|
||||
}
|
||||
4
json_02.json
Normal file
4
json_02.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"a": "foo",
|
||||
"b": "bar"
|
||||
}
|
||||
5
json_03.json
Normal file
5
json_03.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"a": "foo",
|
||||
"b": "bar",
|
||||
"c": "bar"
|
||||
}
|
||||
253
lib/core/isolate_manager.dart
Normal file
253
lib/core/isolate_manager.dart
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
import 'dart:mirrors';
|
||||
import 'dart:isolate';
|
||||
import 'dart:async';
|
||||
|
||||
/// ********************** Test ************************* ///
|
||||
void test() {
|
||||
IsolateManager.create<IsolateTemplate>((isoEvent, data) async {
|
||||
if (isoEvent == IsolateEvent.ready) {
|
||||
int id = data as int;
|
||||
IsolateManager.sendBroadcast(IsoMessege('test1'));
|
||||
await Future.delayed(Duration(seconds: 2));
|
||||
IsolateManager.send(id, IsoMessege('test2'));
|
||||
IsolateManager.send(id, IsoMessege('test1'));
|
||||
IsolateManager.exit(id);
|
||||
} else if (isoEvent == IsolateEvent.exit) {
|
||||
print('Isolate is exited.');
|
||||
}
|
||||
print('On Event from Isolate: ${isoEvent.name}');
|
||||
}, (message) {
|
||||
print('On Message from Isolate: ${message.type}');
|
||||
});
|
||||
}
|
||||
|
||||
class IsolateTemplate extends IsolateBase {
|
||||
@override
|
||||
void onReady() async {
|
||||
print('[IsolateTemplate] onReady');
|
||||
addEventListener('test1', onTest1Handler, executeOnce: true);
|
||||
addEventListener('test2', onTest2Handler);
|
||||
}
|
||||
|
||||
void onTest1Handler(Object? data) {
|
||||
print('[IsolateTemplate] OnTest1Handler: $data');
|
||||
}
|
||||
|
||||
void onTest2Handler(Object? data) {
|
||||
print('[IsolateTemplate] OnTest2Handler: $data');
|
||||
}
|
||||
}
|
||||
|
||||
/// ********************** Define *********************** ///
|
||||
enum IsolateEvent { ready, exit }
|
||||
|
||||
class IsoMessege {
|
||||
late String type;
|
||||
late Object? message;
|
||||
IsoMessege(this.type, {this.message});
|
||||
}
|
||||
|
||||
class _IsolateInnerEvent {
|
||||
static const String ready = '_IsolateInnerEvent.Ready';
|
||||
static const String exit = '_IsolateInnerEvent.Exit';
|
||||
}
|
||||
|
||||
class _IsolateConstructData {
|
||||
late int id;
|
||||
late SendPort mainIsoSender;
|
||||
_IsolateConstructData(this.id, this.mainIsoSender);
|
||||
}
|
||||
|
||||
class IsolateManager {
|
||||
IsolateManager._privateConstructor();
|
||||
static final IsolateManager _instance = IsolateManager._privateConstructor();
|
||||
static IsolateManager get to => _instance;
|
||||
|
||||
int _nonce = 0;
|
||||
int get nonce {
|
||||
if (_nonce == double.maxFinite.toInt()) {
|
||||
_nonce = 0;
|
||||
} else {
|
||||
_nonce++;
|
||||
}
|
||||
return _nonce;
|
||||
}
|
||||
|
||||
final Map<int, IsolateBase> _isoMap = {};
|
||||
final Map<int, Map<String, Function(Object?)>> _innerEventMap = {};
|
||||
|
||||
static void create<T extends IsolateBase>(
|
||||
Function(IsolateEvent, Object?) isolateEventListener,
|
||||
Function(IsoMessege) messageListener) async {
|
||||
to._create<T>(isolateEventListener, messageListener);
|
||||
}
|
||||
|
||||
void _create<T extends IsolateBase>(
|
||||
Function(IsolateEvent, Object?) isolateEventListener,
|
||||
Function(IsoMessege) messageListener) async {
|
||||
final isoListener = ReceivePort();
|
||||
var id = nonce;
|
||||
T inst = Activator.createInstance(T, {});
|
||||
_isoMap[id] = inst;
|
||||
_addInnerEvent(id, _IsolateInnerEvent.ready, (message) {
|
||||
inst.isoListener = message as SendPort;
|
||||
isolateEventListener(IsolateEvent.ready, id);
|
||||
});
|
||||
await Isolate.spawn(
|
||||
inst.initialize, _IsolateConstructData(id, isoListener.sendPort));
|
||||
await for (IsoMessege message in isoListener) {
|
||||
if (message.type == _IsolateInnerEvent.exit) {
|
||||
isolateEventListener(IsolateEvent.exit, null);
|
||||
break;
|
||||
} else if (_filterInnerEvent(id, message)) {
|
||||
messageListener(message);
|
||||
}
|
||||
}
|
||||
isoListener.close();
|
||||
_isoMap.remove(id);
|
||||
}
|
||||
|
||||
void _addInnerEvent(int id, String type, Function(Object?) listener) {
|
||||
Map<String, Function(Object?)>? eventMap;
|
||||
if (!_innerEventMap.containsKey(id)) {
|
||||
eventMap = {};
|
||||
_innerEventMap[id] = eventMap;
|
||||
}
|
||||
eventMap![type] = listener;
|
||||
}
|
||||
|
||||
bool _filterInnerEvent(int id, IsoMessege message) {
|
||||
var isNotExist = true;
|
||||
if (_innerEventMap.containsKey(id) &&
|
||||
_innerEventMap[id]!.containsKey(message.type)) {
|
||||
isNotExist = false;
|
||||
_innerEventMap[id]![message.type]!(message.message);
|
||||
}
|
||||
return isNotExist;
|
||||
}
|
||||
|
||||
static void sendBroadcast(IsoMessege message) {
|
||||
var entries = to._isoMap.entries;
|
||||
for (var entry in entries) {
|
||||
entry.value.insertEvent(message);
|
||||
}
|
||||
}
|
||||
|
||||
static void send(int id, IsoMessege message) {
|
||||
var map = to._isoMap;
|
||||
if (map.containsKey(id)) {
|
||||
map[id]?.insertEvent(message);
|
||||
}
|
||||
}
|
||||
|
||||
static void exit(int id) {
|
||||
var map = to._isoMap;
|
||||
if (map.containsKey(id)) {
|
||||
map[id]!.insertEvent(IsoMessege(_IsolateInnerEvent.exit));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _IsolateEventListener {
|
||||
late bool executeOnce;
|
||||
late Function(Object?) listener;
|
||||
_IsolateEventListener(this.listener, this.executeOnce);
|
||||
}
|
||||
|
||||
class IsolateBase {
|
||||
late int id;
|
||||
late ReceivePort _mainIsoListener;
|
||||
late SendPort _mainIsoSender;
|
||||
late SendPort isoListener;
|
||||
final Map<String, List<_IsolateEventListener>> _eventMap = {};
|
||||
final List<_IsolateEventListener> _removeList = [];
|
||||
|
||||
// ignore: library_private_types_in_public_api
|
||||
Future<void> initialize(_IsolateConstructData initData) async {
|
||||
id = initData.id;
|
||||
_mainIsoListener = ReceivePort();
|
||||
_mainIsoSender = initData.mainIsoSender;
|
||||
send(IsoMessege(_IsolateInnerEvent.ready,
|
||||
message: _mainIsoListener.sendPort));
|
||||
onReady();
|
||||
await for (IsoMessege message in _mainIsoListener) {
|
||||
if (message.type == _IsolateInnerEvent.exit) {
|
||||
send(IsoMessege(_IsolateInnerEvent.exit));
|
||||
break;
|
||||
} else {
|
||||
_onEvent(message);
|
||||
}
|
||||
}
|
||||
Isolate.exit(_mainIsoSender);
|
||||
}
|
||||
|
||||
void onReady() async {}
|
||||
|
||||
void send(IsoMessege message) {
|
||||
_mainIsoSender.send(message);
|
||||
}
|
||||
|
||||
void _onEvent(IsoMessege message) {
|
||||
if (_eventMap.containsKey(message.type)) {
|
||||
var list = _eventMap[message.type]!;
|
||||
for (var item in list) {
|
||||
item.listener(message.message);
|
||||
if (item.executeOnce) {
|
||||
_removeList.add(item);
|
||||
}
|
||||
}
|
||||
if (_removeList.isNotEmpty) {
|
||||
for (var item in _removeList) {
|
||||
removeEventListener(message.type, item.listener);
|
||||
}
|
||||
_removeList.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void addEventListener(String type, Function(Object?) listener,
|
||||
{bool executeOnce = true}) {
|
||||
List<_IsolateEventListener> list;
|
||||
if (_eventMap.containsKey(type)) {
|
||||
list = _eventMap[type]!;
|
||||
} else {
|
||||
list = [];
|
||||
_eventMap[type] = list;
|
||||
}
|
||||
list.add(_IsolateEventListener(listener, executeOnce));
|
||||
}
|
||||
|
||||
void removeEventListener(String type, Function(Object?) listener) {
|
||||
if (_eventMap.containsKey(type)) {
|
||||
var list = _eventMap[type]!;
|
||||
for (int i = 0; i < list.length; ++i) {
|
||||
var item = list[i];
|
||||
if (item.listener == listener) {
|
||||
list.removeAt(i);
|
||||
--i;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void insertEvent(IsoMessege message) {
|
||||
isoListener.send(message);
|
||||
}
|
||||
}
|
||||
|
||||
class Activator {
|
||||
static createInstance(Type type, Map<Symbol, dynamic> namedArguments,
|
||||
[Symbol? constructor, List? arguments]) {
|
||||
constructor ??= const Symbol("");
|
||||
arguments ??= const [];
|
||||
|
||||
var typeMirror = reflectType(type);
|
||||
if (typeMirror is ClassMirror) {
|
||||
return typeMirror
|
||||
.newInstance(constructor, arguments, namedArguments)
|
||||
.reflectee;
|
||||
} else {
|
||||
throw ArgumentError("Cannot create the instance of the type '$type'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,10 @@
|
|||
void log(String message) {}
|
||||
import 'package:dart_framework/core/isolate_manager.dart';
|
||||
import 'package:dart_framework/io/log_writer_base.dart';
|
||||
import 'package:dart_framework/utils/string_util.dart';
|
||||
|
||||
void log(String message) {
|
||||
_logSend(message, LogType.verbose);
|
||||
}
|
||||
|
||||
void logWarning(String message) {
|
||||
_logSend(message, LogType.warning);
|
||||
|
|
@ -21,23 +27,49 @@ class LogItem {
|
|||
late LogType type;
|
||||
|
||||
LogItem(this.message, this.type) {
|
||||
var now = DateTime.now();
|
||||
date =
|
||||
'${now.year.toString().substring(2, 4)}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')} '
|
||||
'${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}:${now.second.toString().padLeft(2, '0')}';
|
||||
date = getDates();
|
||||
}
|
||||
@override
|
||||
String toString() {
|
||||
return '[$date][${type.name}] $message';
|
||||
}
|
||||
}
|
||||
|
||||
class Log {
|
||||
Log._privateConstructor();
|
||||
int index = 0;
|
||||
StringBuffer buffer = StringBuffer();
|
||||
Log._privateConstructor() {
|
||||
LogWriterBase.start(_onWriterEvent);
|
||||
}
|
||||
static final Log _instance = Log._privateConstructor();
|
||||
static Log get to => _instance;
|
||||
|
||||
final List<LogItem> logs = [];
|
||||
|
||||
Log();
|
||||
Log() {
|
||||
LogWriterBase.start(_onWriterEvent);
|
||||
}
|
||||
|
||||
void _onWriterEvent(IsoMessege message) {
|
||||
if (message.type == 'request') {
|
||||
if (index != logs.length) {
|
||||
for (int i = logs.length - index - 1; i >= 0; --i) {
|
||||
buffer.writeln(logs[i].toString());
|
||||
}
|
||||
index = logs.length;
|
||||
LogWriterBase.write(buffer.toString());
|
||||
buffer.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void append(LogItem item) {
|
||||
logs.insert(0, item);
|
||||
}
|
||||
|
||||
void clear() {
|
||||
logs.clear();
|
||||
index = 0;
|
||||
log('Log Restarted.');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
10
lib/io/file_base.dart
Normal file
10
lib/io/file_base.dart
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import 'package:dart_framework/io/file_io.dart'
|
||||
if (dart.library.html) 'package:dart_framework/io/file_web.dart';
|
||||
|
||||
abstract class FileExtendBase {
|
||||
static FileExtendBase get instance {
|
||||
return getInstance();
|
||||
}
|
||||
|
||||
void test();
|
||||
}
|
||||
13
lib/io/file_io.dart
Normal file
13
lib/io/file_io.dart
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import 'dart:io';
|
||||
import 'package:dart_framework/io/file_base.dart';
|
||||
|
||||
class FileExtend extends FileExtendBase {
|
||||
@override
|
||||
void test() {
|
||||
print('This is ${Platform.operatingSystem}');
|
||||
}
|
||||
}
|
||||
|
||||
FileExtendBase getInstance() {
|
||||
return FileExtend();
|
||||
}
|
||||
17
lib/io/file_web.dart
Normal file
17
lib/io/file_web.dart
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import 'dart:html';
|
||||
import 'package:dart_framework/io/file_base.dart';
|
||||
|
||||
class FileExtend extends FileExtendBase {
|
||||
@override
|
||||
void test() {
|
||||
var list = querySelectorAll('div');
|
||||
for (var item in list) {
|
||||
print(item);
|
||||
}
|
||||
print('This is web!!');
|
||||
}
|
||||
}
|
||||
|
||||
FileExtendBase getInstance() {
|
||||
return FileExtend();
|
||||
}
|
||||
13
lib/io/log_writer_base.dart
Normal file
13
lib/io/log_writer_base.dart
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import 'package:dart_framework/core/isolate_manager.dart';
|
||||
import 'package:dart_framework/io/log_writer_io.dart'
|
||||
if (dart.library.html) 'package:dart_framework/io/log_writer_web.dart';
|
||||
|
||||
abstract class LogWriterBase extends IsolateBase {
|
||||
static void start(Function(IsoMessege) listener) {
|
||||
startLog(listener);
|
||||
}
|
||||
|
||||
static void write(String log) {
|
||||
print(log);
|
||||
}
|
||||
}
|
||||
27
lib/io/log_writer_io.dart
Normal file
27
lib/io/log_writer_io.dart
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import 'dart:io';
|
||||
import 'package:dart_framework/core/isolate_manager.dart';
|
||||
import 'package:dart_framework/io/log_writer_base.dart';
|
||||
|
||||
class LogWriterIO extends LogWriterBase {
|
||||
@override
|
||||
void onReady() {
|
||||
print('[LogWriterIO] onReady');
|
||||
addEventListener('write', write);
|
||||
requester();
|
||||
}
|
||||
|
||||
void requester() async {
|
||||
while (true) {
|
||||
await Future.delayed(const Duration(seconds: 3));
|
||||
send(IsoMessege('request'));
|
||||
}
|
||||
}
|
||||
|
||||
void write(Object? message) {
|
||||
print('write');
|
||||
}
|
||||
}
|
||||
|
||||
void startLog(Function(IsoMessege) listener) {
|
||||
IsolateManager.create<LogWriterIO>((event, data) {}, listener);
|
||||
}
|
||||
1
lib/io/log_writer_web.dart
Normal file
1
lib/io/log_writer_web.dart
Normal file
|
|
@ -0,0 +1 @@
|
|||
void startLog() {}
|
||||
28
lib/test/isolate_test.dart
Normal file
28
lib/test/isolate_test.dart
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
// Read the file, spawn an isolate, send the file contents to the spawned
|
||||
// isolate, and wait for the parsed JSON.
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
|
||||
const filename = 'json_01.json';
|
||||
|
||||
Future<void> main() async {
|
||||
// Read and parse JSON data in a new isolate,
|
||||
// then store the returned Dart representation.
|
||||
final jsonData = await Isolate.run(() => _readAndParseJson(filename));
|
||||
|
||||
print('Received JSON with ${jsonData.length} keys');
|
||||
}
|
||||
|
||||
/// Reads the contents of the file with [filename],
|
||||
/// decodes the JSON, and returns the result.
|
||||
Future<Map<String, dynamic>> _readAndParseJson(String filename) async {
|
||||
final fileData = await File(filename).readAsString();
|
||||
final jsonData = jsonDecode(fileData) as Map<String, dynamic>;
|
||||
return jsonData;
|
||||
}
|
||||
90
lib/test/test2.dart
Normal file
90
lib/test/test2.dart
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
// Copyright (c) 2021, the Dart project authors. Please see the AUTHORS file
|
||||
// for details. All rights reserved. Use of this source code is governed by a
|
||||
// BSD-style license that can be found in the LICENSE file.
|
||||
|
||||
// Spawn an isolate, read multiple files, send their contents to the spawned
|
||||
// isolate, and wait for the parsed JSON.
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:async/async.dart';
|
||||
|
||||
const filenames = [
|
||||
'json_01.json',
|
||||
'json_02.json',
|
||||
'json_03.json',
|
||||
];
|
||||
|
||||
void start() async {
|
||||
await for (final jsonData in _sendAndReceive(filenames)) {
|
||||
print('Received JSON with ${jsonData.length} keys');
|
||||
}
|
||||
}
|
||||
|
||||
// Spawns an isolate and asynchronously sends a list of filenames for it to
|
||||
// read and decode. Waits for the response containing the decoded JSON
|
||||
// before sending the next.
|
||||
//
|
||||
// Returns a stream that emits the JSON-decoded contents of each file.
|
||||
Stream<Map<String, dynamic>> _sendAndReceive(List<String> filenames) async* {
|
||||
final p = ReceivePort();
|
||||
await Isolate.spawn(_readAndParseJsonService, p.sendPort);
|
||||
|
||||
// Convert the ReceivePort into a StreamQueue to receive messages from the
|
||||
// spawned isolate using a pull-based interface. Events are stored in this
|
||||
// queue until they are accessed by `events.next`.
|
||||
final events = StreamQueue<dynamic>(p);
|
||||
|
||||
// The first message from the spawned isolate is a SendPort. This port is
|
||||
// used to communicate with the spawned isolate.
|
||||
SendPort sendPort = await events.next;
|
||||
|
||||
for (var filename in filenames) {
|
||||
// Send the next filename to be read and parsed
|
||||
sendPort.send(filename);
|
||||
|
||||
// Receive the parsed JSON
|
||||
Map<String, dynamic> message = await events.next;
|
||||
|
||||
// Add the result to the stream returned by this async* function.
|
||||
yield message;
|
||||
}
|
||||
|
||||
// Send a signal to the spawned isolate indicating that it should exit.
|
||||
sendPort.send(null);
|
||||
|
||||
// Dispose the StreamQueue.
|
||||
await events.cancel();
|
||||
}
|
||||
|
||||
// The entrypoint that runs on the spawned isolate. Receives messages from
|
||||
// the main isolate, reads the contents of the file, decodes the JSON, and
|
||||
// sends the result back to the main isolate.
|
||||
Future<void> _readAndParseJsonService(SendPort p) async {
|
||||
print('Spawned isolate started.');
|
||||
|
||||
// Send a SendPort to the main isolate so that it can send JSON strings to
|
||||
// this isolate.
|
||||
final commandPort = ReceivePort();
|
||||
p.send(commandPort.sendPort);
|
||||
|
||||
// Wait for messages from the main isolate.
|
||||
await for (final message in commandPort) {
|
||||
if (message is String) {
|
||||
// Read and decode the file.
|
||||
final contents = await File(message).readAsString();
|
||||
|
||||
// Send the result to the main isolate.
|
||||
p.send(jsonDecode(contents));
|
||||
} else if (message == null) {
|
||||
// Exit if the main isolate sends a null message, indicating there are no
|
||||
// more files to read and parse.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
print('Spawned isolate finished.');
|
||||
Isolate.exit();
|
||||
}
|
||||
|
|
@ -1,3 +1,17 @@
|
|||
String toPascal(String raw) {
|
||||
return '${raw[0].toUpperCase()}${raw.substring(1).toLowerCase()}';
|
||||
}
|
||||
|
||||
String getDates() {
|
||||
return '$getDate() $getTime()';
|
||||
}
|
||||
|
||||
String getDate() {
|
||||
var now = DateTime.now();
|
||||
return '${now.year.toString().substring(2, 4)}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
String getTime() {
|
||||
var now = DateTime.now();
|
||||
return '${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}:${now.second.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue